Skip to content

Commit c3f00e3

Browse files
committed
Optimize #with
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.
1 parent 121e18d commit c3f00e3

File tree

1 file changed

+20
-5
lines changed

1 file changed

+20
-5
lines changed

lib/values.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ def initialize(*values)
4949
const_set :VALUE_ATTRS, fields
5050

5151
def self.with(hash)
52-
unexpected_keys = hash.keys - self::VALUE_ATTRS
53-
if unexpected_keys.any?
52+
num_recognized_keys = self::VALUE_ATTRS.count { |field| hash.key?(field) }
53+
54+
if num_recognized_keys != hash.size
55+
unexpected_keys = hash.keys - self::VALUE_ATTRS
5456
raise ArgumentError.new("Unexpected hash keys: #{unexpected_keys}")
5557
end
5658

57-
missing_keys = self::VALUE_ATTRS - hash.keys
58-
if missing_keys.any?
59+
if num_recognized_keys != self::VALUE_ATTRS.size
60+
missing_keys = self::VALUE_ATTRS - hash.keys
5961
raise ArgumentError.new("Missing hash keys: #{missing_keys} (got keys #{hash.keys})")
6062
end
6163

@@ -94,9 +96,22 @@ def pretty_print(q)
9496
end
9597
end
9698

99+
# Optimized to avoid intermediate Hash instantiations.
97100
def with(hash = {})
98101
return self if hash.empty?
99-
self.class.with(to_h.merge(hash))
102+
103+
num_recognized_keys = self.class::VALUE_ATTRS.count { |field| hash.key?(field) }
104+
105+
if num_recognized_keys != hash.size
106+
unexpected_keys = hash.keys - self.class::VALUE_ATTRS
107+
raise ArgumentError.new("Unexpected hash keys: #{unexpected_keys}")
108+
end
109+
110+
args = self.class::VALUE_ATTRS.map do |field|
111+
hash.key?(field) ? hash[field] : send(field)
112+
end
113+
114+
self.class.new(*args)
100115
end
101116

102117
def to_h

0 commit comments

Comments
 (0)