Skip to content

Commit 15c0489

Browse files
committed
Optimize #with
This PR is inspired by tcrayford#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 8b78d97 commit 15c0489

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
@@ -38,13 +38,15 @@ def self.new(*fields, &block)
3838
const_set :VALUE_ATTRS, fields
3939

4040
def self.with(hash)
41-
unexpected_keys = hash.keys - self::VALUE_ATTRS
42-
if unexpected_keys.any?
41+
num_recognized_keys = self::VALUE_ATTRS.count { |field| hash.key?(field) }
42+
43+
if num_recognized_keys != hash.size
44+
unexpected_keys = hash.keys - self::VALUE_ATTRS
4345
raise ArgumentError.new("Unexpected hash keys: #{unexpected_keys}")
4446
end
4547

46-
missing_keys = self::VALUE_ATTRS - hash.keys
47-
if missing_keys.any?
48+
if num_recognized_keys != self::VALUE_ATTRS.size
49+
missing_keys = self::VALUE_ATTRS - hash.keys
4850
raise ArgumentError.new("Missing hash keys: #{missing_keys} (got keys #{hash.keys})")
4951
end
5052

@@ -83,9 +85,22 @@ def pretty_print(q)
8385
end
8486
end
8587

88+
# Optimized to avoid intermediate Hash instantiations.
8689
def with(hash = {})
8790
return self if hash.empty?
88-
self.class.with(to_h.merge(hash))
91+
92+
num_recognized_keys = self.class::VALUE_ATTRS.count { |field| hash.key?(field) }
93+
94+
if num_recognized_keys != hash.size
95+
unexpected_keys = hash.keys - self.class::VALUE_ATTRS
96+
raise ArgumentError.new("Unexpected hash keys: #{unexpected_keys}")
97+
end
98+
99+
args = self.class::VALUE_ATTRS.map do |field|
100+
hash.key?(field) ? hash[field] : send(field)
101+
end
102+
103+
self.class.new(*args)
89104
end
90105

91106
def to_h

0 commit comments

Comments
 (0)