-
Notifications
You must be signed in to change notification settings - Fork 375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use +@
instead of dup
for duplicating strings
#2704
Conversation
👋 @nirebu , I am aware that In Ruby 2.3 or later unary plus operator is faster than Our team is currently planning the deprecation for those old rubies, perhaps we would introduce the changes by then |
@TonyCTHsu thanks for getting back. Totally agree with you, judging by the broken tests I can guess this is not the right time for this. Closing this then :) Will get back to it in the future! |
@nirebu, given this is a hot code path, could you add a guard around old ruby versions and actually implement this for newer rubies? Something like: # Introduced in Ruby 2.3
if String.method_defined?(:+@)
def self.frozen_or_dup(v)
v.frozen? ? v : +v
end
else
def self.frozen_or_dup(v)
v.frozen? ? v : v.dup
end
end |
81f57d7
to
eb2d638
Compare
dup
for duplicating strings+@
instead of dup
for duplicating strings
@marcotc I've updated my PR with your suggestion, however the failing specs tell a different story: it seems that it is mandatory to have a different Given how I have yet to dig in how these Strings are used downstream, but it seems that we want different objects given this comment. EDIT: I've read #1783 's discussion, and it seems that changing the behaviour to use |
Hey, @nirebu you are completely right, and sorry for us not catching this earlier. We could technically use One issue might be related to users of the the Processing Pipeline, given you are allowed to arbitrarily modify the span: it's possible users are performing modifying operation on Strings belonging to a trace or span. |
cf257fe
to
615ebe5
Compare
@marcotc I've updated my PR (and main comment). The full explanation is in the commit message and main comment, however I think I've found a way to squeeze a bit more performance while keeping the same contract for the helper: if v is frozen, return it, otherwise return a non-frozen copy (which may end up or not end up frozen where it's used). Another optimisation I can think of would be to have a specialised helper to use in the |
# String#+@ was introduced in Ruby 2.3 | ||
if String.method_defined?(:+@) && String.method_defined?(:-@) | ||
def self.frozen_or_dup(v) | ||
v.frozen? ? v : +(-v) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nirebu, can you leave a comment here explaining why +(-v)
is being done here? I think this will raise questions in the future when someone stumbles upon this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcotc added!
@nirebu, the |
String#+@ was introduced in Ruby 2.3: it is faster and a bit cheaper on the memory side as well than #dup. However, to retain the helper contract we need to return "thawed" copies of the original values, so we need to first call String#-@ to return a frozen copy, then call String#+@ to effectively have an unfrozen copy of the original value. require "bundler/inline" gemfile(true) do source "https://rubygems.org" gem "benchmark-ips" gem "benchmark-memory" end Benchmark.ips do |x| x.report("+(-@)") { v = "foo"; +(-v) } x.report("dup") { v = "foo"; v.dup } x.compare! end Benchmark.memory do |x| x.report("+(-@)") { v = "foo"; +(-v) } x.report("dup") { v = "foo"; v.dup } x.compare! end Warming up -------------------------------------- +(-@) 781.717k i/100ms dup 711.791k i/100ms Calculating ------------------------------------- +(-@) 7.801M (± 0.8%) i/s - 39.086M in 5.010682s dup 7.138M (± 0.7%) i/s - 36.301M in 5.085850s Comparison: +(-@): 7800971.1 i/s dup: 7138065.1 i/s - 1.09x slower Calculating ------------------------------------- +(-@) 80.000 memsize ( 0.000 retained) 2.000 objects ( 0.000 retained) 1.000 strings ( 0.000 retained) dup 80.000 memsize ( 0.000 retained) 2.000 objects ( 0.000 retained) 1.000 strings ( 0.000 retained) Comparison: +(-@): 80 allocated dup: 80 allocated - same
In the Datadog::Tracing::Correlation::Identifier the unfrozen copies of strings are then frozen again: that's a step that can be skipped if the original value is already frozen, so we can save allocations. require "bundler/inline" gemfile(true) do source "https://rubygems.org" gem "benchmark-ips" gem "benchmark-memory" end Benchmark.ips do |x| x.report("+(-@).freeze") { v = "foo"; c = +(-v).freeze } x.report("-v") { v = "foo"; c = -v } x.compare! end Benchmark.memory do |x| x.report("+(-@).freeze") { v = "foo"; c = +(-v).freeze } x.report("-v") { v = "foo"; c = -v } x.compare! end Warming up -------------------------------------- +(-@).freeze 691.075k i/100ms -v 886.370k i/100ms Calculating ------------------------------------- +(-@).freeze 6.965M (± 0.5%) i/s - 35.245M in 5.060545s -v 8.839M (± 0.6%) i/s - 44.318M in 5.014288s Comparison: -v: 8838792.7 i/s +(-@).freeze: 6964819.0 i/s - 1.27x slower Calculating ------------------------------------- +(-@).freeze 80.000 memsize ( 0.000 retained) 2.000 objects ( 0.000 retained) 1.000 strings ( 0.000 retained) -v 40.000 memsize ( 0.000 retained) 1.000 objects ( 0.000 retained) 1.000 strings ( 0.000 retained) Comparison: -v: 40 allocated +(-@).freeze: 80 allocated - 2.00x more
615ebe5
to
1527ed8
Compare
@marcotc I should have addressed all feedback. |
@marcotc is there anything else I can do here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much, @nirebu! 🙇
String#+@ was introduced in Ruby 2.3: it is faster and a bit cheaper on
the memory side as well than #dup. However, to retain the helper contract
we need to return "thawed" copies of the original values, so we need to
first call String#-@ to return a frozen copy, then call String#+@ to
effectively have an unfrozen copy of the original value.
What does this PR do?
Changes how the gem duplicates frozen strings, in favor of a more performant method.
Motivation
I was benchmarking a client application and found the modified line as a source of retained strings. Even if I couldn't replicate the issue in the synthetic benchmark, this change reduced the number of retained strings coming from that allocation.
Additional Notes
Additional ref: rails/rails@1b86d90
How to test the change?
Please find above the code for benchmarking the change.