Skip to content

Commit f3527f8

Browse files
ms-atitomeon
authored andcommitted
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 14db20e commit f3527f8

File tree

1 file changed

+21
-5
lines changed

1 file changed

+21
-5
lines changed

lib/values.rb

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,15 @@ def self.new(*fields, &block)
6060
const_set :VALUE_ATTRS, fields
6161

6262
def self.with(hash)
63-
unexpected_fields = hash.keys - self::VALUE_ATTRS
64-
missing_fields = self::VALUE_ATTRS - hash.keys
63+
num_recognized_keys = self::VALUE_ATTRS.count { |field| hash.key?(field) }
6564

66-
if unexpected_fields.any?
65+
if num_recognized_keys != hash.size
66+
unexpected_fields = hash.keys - self::VALUE_ATTRS
67+
missing_fields = self::VALUE_ATTRS - hash.keys
6768
raise Values::FieldError.new("Unexpected hash keys: #{unexpected_fields}", missing_fields:, unexpected_fields:)
68-
elsif missing_fields.any?
69+
elsif num_recognized_keys != self::VALUE_ATTRS.size
70+
unexpected_fields = hash.keys - self::VALUE_ATTRS
71+
missing_fields = self::VALUE_ATTRS - hash.keys
6972
raise Values::FieldError.new("Missing hash keys: #{missing_fields} (got keys #{hash.keys})", missing_fields:, unexpected_fields:)
7073
end
7174

@@ -104,9 +107,22 @@ def pretty_print(q)
104107
end
105108
end
106109

110+
# Optimized to avoid intermediate Hash instantiations.
107111
def with(hash = {})
108112
return self if hash.empty?
109-
self.class.with(to_h.merge(hash))
113+
114+
num_recognized_keys = self.class::VALUE_ATTRS.count { |field| hash.key?(field) }
115+
116+
if num_recognized_keys != hash.size
117+
unexpected_fields = hash.keys - self.class::VALUE_ATTRS
118+
raise Values::FieldError.new("Unexpected hash keys: #{unexpected_fields}", unexpected_fields:)
119+
end
120+
121+
args = self.class::VALUE_ATTRS.map do |field|
122+
hash.key?(field) ? hash[field] : send(field)
123+
end
124+
125+
self.class.new(*args)
110126
end
111127

112128
def to_h

0 commit comments

Comments
 (0)