Hi all
I’ve been reading through the Rails code over the holidays and noticed an opportunity for a possible reactor that could improve performance in some cases.
I’ve pushed my changes out to Github (on my own fork) but—as directed in the Contribution Guidelines—I’m opening the topic for discussion here first.
Please feel free to review the changes I’ve made in this commit:
My thinking was that getting values from an OrderedOptions instance is probably more common than either setting a value or getting with a bang. I think this is particularly true while code is being executed at runtime, since things like configuration values tend not to change once the app has booted.
I tried a few permutations to optimise for this case, and benchmarked them using Benchmark/IPS.
The commit I’ve linked above was the best performer I could come up with today.
Here are some benchmark results from my own machine. In all cases, only the method_missing
method was changed. I’ve omitted the rest of the methods from the examples for brevity.
# Current implementation
class VariantA < Hash
# ...
def method_missing(name, *args)
name_string = +name.to_s
if name_string.chomp!("=")
self[name_string] = args.first
else
bangs = name_string.chomp!("!")
if bangs
self[name_string].presence || raise(KeyError.new(":#{name_string} is blank"))
else
self[name_string]
end
end
end
# ...
end
# Fastest
class VariantB < Hash
# ...
def method_missing(name, *args)
return self[name] unless name.end_with?("=") || name.end_with?("!")
name_string = name.to_s
if name_string.slice!(-1) == "="
return self[name_string] = args.first
end
self[name_string].presence || raise(KeyError.new(":#{name_string} is blank"))
end
# ...
end
class VariantC < Hash
# ...
def method_missing(name, *args)
return self[name] unless name.end_with?("=") || name.end_with?("!")
name_string = name.to_s
name_string.slice!(-1)
if name.end_with?("=")
return self[name_string] = args.first
end
self[name_string].presence || raise(KeyError.new(":#{name_string} is blank"))
end
# ...
end
test_lambda = lambda do |test_class|
h = test_class.new
# Extracted from example in docs
h.boy = 'John'
h.girl = 'Mary'
h.boy
h.girl
h.dog
end
Benchmark.ips do |test|
test.iterations = 1
test.report("Variant A") do
test_lambda.call(VariantA)
end
test.report("Variant B") do
test_lambda.call(VariantB)
end
test.report("Variant C") do
test_lambda.call(VariantC)
end
test.compare!
end
Results
The benchmark results on my machine look like this:
Warming up --------------------------------------
Variant A 16.082k i/100ms
Variant B 20.174k i/100ms
Variant C 19.369k i/100ms
Calculating -------------------------------------
Variant A 164.454k (± 2.9%) i/s - 836.264k in 5.089449s
Variant B 201.225k (± 1.7%) i/s - 1.009M in 5.014209s
Variant C 192.310k (± 2.0%) i/s - 968.450k in 5.037718s
Comparison:
Variant B: 201225.1 i/s
Variant C: 192310.2 i/s - 1.05x (± 0.00) slower
Variant A: 164453.9 i/s - 1.22x (± 0.00) slower
I would welcome feedback on this, and whether or not the community would welcome a PR with these changes.
Thanks, and happy New Year!