ruby / psych

A libyaml wrapper for Ruby
MIT License
566 stars 206 forks source link

Improve `YAMLTree` performance by not using `object_id`s #663

Closed amomchilov closed 11 months ago

amomchilov commented 11 months ago

Since Ruby 2.7, object_id became more expensive, particularly on its first call for a particular object. @JemmaIssroff has a great blog post about it.

We can reduce how many objects we assign object IDs to, by using Hash#compare_by_identity in the YAMLTree::Registrar. The semantics will be the same, but now loads/stores into the Hash are faster, and we don't need to trigger lots of object ID assignments.

On a related note, I used assert_same in the tests, where applicable (instead of assert_equal a.object_id, b.object_id).

Is there a benchmark suite I can run to compare before/after performance?

tenderlove commented 11 months ago

Great find, thank you!

amomchilov commented 11 months ago

Put this tiny synthetic benchmark together and woah... 7% improvement on net serialization performance!

Comparison:
  After removing @targets array:     3030.9 i/s
After switching to identity set:     2975.9 i/s - same-ish: difference falls within error
             Before all changes:     2828.5 i/s - 1.07x  slower
benchmark.rb ```ruby def generate_test_data repeated_points = 10.times.map { |i| [i, i] } (repeated_points * 2) + 80.times.map { |i| [i, i] } end def test_perf puts Psych.dump(generate_test_data) points = generate_test_data Benchmark.ips do |x| x.report(" Before all changes") do |times| i = 0 while (i += 1) < times Psych.dump(points.map { |p| p.dup }) end end x.hold!("psych_benchmark1.json") # Then: git checkout 0dc25a9 x.report("After switching to identity set") do |times| i = 0 while (i += 1) < times Psych.dump(points.map { |p| p.dup }) end end x.hold!("psych_benchmark2.json") # Then: git checkout 6905a2 x.report(" After removing @targets array") do |times| i = 0 while (i += 1) < times Psych.dump(points.map { |p| p.dup }) end end x.compare! end end ```

The difference is emphasized here, I chose objects that do minimal other serialization (e.g. simple arrays, no custom class with long names, and cheap field values).