Skip to content

Commit

Permalink
Optimize #with
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ms-ati authored and tomeon committed Apr 21, 2024
1 parent 14db20e commit f3527f8
Showing 1 changed file with 21 additions and 5 deletions.
26 changes: 21 additions & 5 deletions lib/values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,15 @@ def self.new(*fields, &block)
const_set :VALUE_ATTRS, fields

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

if unexpected_fields.any?
if num_recognized_keys != hash.size
unexpected_fields = hash.keys - self::VALUE_ATTRS
missing_fields = self::VALUE_ATTRS - hash.keys
raise Values::FieldError.new("Unexpected hash keys: #{unexpected_fields}", missing_fields:, unexpected_fields:)
elsif missing_fields.any?
elsif num_recognized_keys != self::VALUE_ATTRS.size
unexpected_fields = hash.keys - self::VALUE_ATTRS
missing_fields = self::VALUE_ATTRS - hash.keys
raise Values::FieldError.new("Missing hash keys: #{missing_fields} (got keys #{hash.keys})", missing_fields:, unexpected_fields:)
end

Expand Down Expand Up @@ -104,9 +107,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_fields = hash.keys - self.class::VALUE_ATTRS
raise Values::FieldError.new("Unexpected hash keys: #{unexpected_fields}", unexpected_fields:)
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 f3527f8

Please sign in to comment.