Skip to content

Commit

Permalink
Optimize #with
Browse files Browse the repository at this point in the history
This PR is inspired by #56, and
assumes that code will be merged, so uses it in the benchmarks here:
https://gist.github.com/ms-ati/fa8002ef8a0ce00716e9aa6510d3d4d9

It is common in our code, as in any idiomatic code using value objects
in loops or pipelines, to call `#with` many times, returning a new immutable
object each time with 1 or more fields replaced with new values.

The optimizations in this PR eliminate a number of extra Hash and Array
instantiations that were occurring each time, in favor of iterating only over
the constant `VALUE_ATTRS` array and doing key lookups in the given
Hash parameter in the hot paths.

Per the gist above, this increases ips (iterations per second) 2.29x, from
335.9 to 769.6 on my machine.
  • Loading branch information
ms-ati committed Mar 22, 2017
1 parent 8b78d97 commit 15c0489
Showing 1 changed file with 20 additions and 5 deletions.
25 changes: 20 additions & 5 deletions lib/values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ def self.new(*fields, &block)
const_set :VALUE_ATTRS, fields

def self.with(hash)
unexpected_keys = hash.keys - self::VALUE_ATTRS
if unexpected_keys.any?
num_recognized_keys = self::VALUE_ATTRS.count { |field| hash.key?(field) }

if num_recognized_keys != hash.size
unexpected_keys = hash.keys - self::VALUE_ATTRS
raise ArgumentError.new("Unexpected hash keys: #{unexpected_keys}")
end

missing_keys = self::VALUE_ATTRS - hash.keys
if missing_keys.any?
if num_recognized_keys != self::VALUE_ATTRS.size
missing_keys = self::VALUE_ATTRS - hash.keys
raise ArgumentError.new("Missing hash keys: #{missing_keys} (got keys #{hash.keys})")
end

Expand Down Expand Up @@ -83,9 +85,22 @@ def pretty_print(q)
end
end

# Optimized to avoid intermediate Hash instantiations.
def with(hash = {})
return self if hash.empty?
self.class.with(to_h.merge(hash))

num_recognized_keys = self.class::VALUE_ATTRS.count { |field| hash.key?(field) }

if num_recognized_keys != hash.size
unexpected_keys = hash.keys - self.class::VALUE_ATTRS
raise ArgumentError.new("Unexpected hash keys: #{unexpected_keys}")
end

args = self.class::VALUE_ATTRS.map do |field|
hash.key?(field) ? hash[field] : send(field)
end

self.class.new(*args)
end

def to_h
Expand Down

0 comments on commit 15c0489

Please sign in to comment.