Skip to content

Instantly share code, notes, and snippets.

@ms-ati
Created March 22, 2017 14:10
Show Gist options
  • Save ms-ati/fa8002ef8a0ce00716e9aa6510d3d4d9 to your computer and use it in GitHub Desktop.
Save ms-ati/fa8002ef8a0ce00716e9aa6510d3d4d9 to your computer and use it in GitHub Desktop.
Benchmarks showing optimization of `#with` in Values gem
# Simple immutable value objects for ruby.
#
# @example Make a new value class:
# Point = Value.new(:x, :y)
#
# @example And use it:
# p = Point.new(1, 0)
# p.x
# #=> 1
# p.y
# #=> 0
#
class Value
# Create a new value class.
#
# @param [Array<Symbol>] fields Names of fields to create in the new value class
# @param [Proc] block Optionally, a block to further define the new value class
# @return [Class] A new value class with the provided `fields`
# @raise [ArgumentError] If no field names are provided
def self.new(*fields, &block)
raise ArgumentError.new('wrong number of arguments (0 for 1+)') if fields.empty?
Class.new do
attr_reader(:hash, *fields)
instance_var_assignments = Array.new(fields.length) do |idx|
"@#{fields[idx]} = values[#{idx}]"
end.join("\n")
class_eval <<-RUBY
def initialize(*values)
if #{fields.size} != values.size
raise ArgumentError.new("wrong number of arguments, \#{values.size} for #{fields.size}")
end
#{instance_var_assignments}
@hash = self.class.hash ^ values.hash
freeze
end
RUBY
const_set :VALUE_ATTRS, fields
def self.with(hash)
num_recognized_keys = self::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
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
new(*hash.values_at(*self::VALUE_ATTRS))
end
def ==(other)
eql?(other)
end
def eql?(other)
self.class == other.class && values == other.values
end
def values
self.class::VALUE_ATTRS.map { |field| send(field) }
end
def inspect
attributes = to_a.map { |field, value| "#{field}=#{value.inspect}" }.join(', ')
"#<#{self.class.name} #{attributes}>"
end
def pretty_print(q)
q.group(1, "#<#{self.class.name}", '>') do
q.seplist(to_a, lambda { q.text ',' }) do |pair|
field, value = pair
q.breakable
q.text field.to_s
q.text '='
q.group(1) do
q.breakable ''
q.pp value
end
end
end
end
def with(hash = {})
return self if hash.empty?
self.class.with(to_h.merge(hash))
end
# NOTE: This is the proposed new implementation of `#with` being benchmarked
# This code assumes the new optimized initializer from https://github.com/tcrayford/Values/pull/56
# as a baseline.
def with2(hash = {})
return self if hash.empty?
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
Hash[to_a]
end
def recursive_to_h
Hash[to_a.map{|k, v| [k, Value.coerce_to_h(v)]}]
end
def to_a
self.class::VALUE_ATTRS.map { |field| [field, send(field)] }
end
class_eval &block if block
end
end
protected
def self.coerce_to_h(v)
case
when v.is_a?(Hash)
Hash[v.map{|hk, hv| [hk, coerce_to_h(hv)]}]
when v.respond_to?(:map)
v.map{|x| coerce_to_h(x)}
when v && v.respond_to?(:to_h)
v.to_h
else
v
end
end
end
require "benchmark/ips"
fields = [:a, :b, :c, :d, :e, :f, :g]
c = Value.new(*fields)
o = c.new("aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg")
Benchmark.ips do |x|
x.report("value") { o.with(e: "eeee", g: "gggg") }
x.report("value2") { o.with2(e: "eeee", g: "gggg") }
end
i1 = o.with(e: "eeee", g: "gggg")
i2 = o.with2(e: "eeee", g: "gggg")
puts i1.to_h == i2.to_h
# Warming up --------------------------------------
# value 6.220k i/100ms
# value2 13.743k i/100ms
# Calculating -------------------------------------
# value 67.225k (± 3.5%) i/s - 335.880k in 5.002485s
# value2 151.675k (± 2.8%) i/s - 769.608k in 5.078078s
# true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment