Skip to content
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

Lesser '.' objects for number helpers #24502

Merged
merged 2 commits into from
Apr 12, 2016

Conversation

ankit8898
Copy link
Contributor

Summary

ActiveSupport::NumberHelper number_to_delimited and number_to_rounded methods were creating duplicate dot ('.') strings in my project. When used with a iterating loop lot of string allocation happens. Debugging done via derailed_benchmarks

Here is a simplified example:

number_to_delimited

class FooController < ApplicationController
  def index
    #Notice here we are iterating 1000 times
    render json: 1000.times.collect { ActiveSupport::NumberHelper.number_to_delimited(1111) }
  end
end

Notice 1000 dot initializations in below report

Allocated String Report
-----------------------------------
    # Snipped ...

      1002  "."
      1000  /Users/agupta/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.6/lib/active_support/number_helper/number_to_delimited_converter.rb:15
         2  /Users/agupta/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.6/lib/active_support/subscriber.rb:99

number_to_rounded

Since number_to_rounded internally calls NumberToDelimitedConverter converter the effect is more.

class FooController < ApplicationController
  def index
   # iterating 1000 times
    render json: 1000.times.collect { ActiveSupport::NumberHelper.number_to_rounded(111.1) }
  end
end

Notice 1000 - 1000 dot initializations in below report for number_to_delimited_converter & number_to_rounded_converter

Allocated String Report
-----------------------------------
      3002  "."
      1000  /Users/agupta/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.6/lib/active_support/number_helper/number_to_delimited_converter.rb:15
      1000  /Users/agupta/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.6/lib/active_support/number_helper/number_to_rounded_converter.rb:33

#Snipped...

After freezing the duplicate string were not formed. cc @schneems

…every iteration of calling the helper. Eases on some memory bloat
@rails-bot
Copy link

r? @kaspth

(@rails-bot has picked a reviewer for you, use r? to override)

@simi
Copy link
Contributor

simi commented Apr 11, 2016

What about to move '.'.freeze to shared constatnt in ActiveSupport::NumberHelper?

PS: I have spotted another strings initializations used in those two files. Aren't they causing the same problem?

@@ -30,7 +30,7 @@ def convert
formatted_string =
if BigDecimal === rounded_number && rounded_number.finite?
s = rounded_number.to_s('F') + '0'*precision
a, b = s.split('.', 2)
a, b = s.split('.'.freeze, 2)
a + '.' + b[0, precision]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could further reduce allocations by appending to the "a" string here using << instead of +. We can do that because split returns a new string. I think we can use the same trick above where we are adding the zeroes to generate "s".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, i actually missed it. It was thr in the report too line 37 a + '.' + b[0, precision]. This is also a problem and overall we have 3000 dot strings

     3002  "."
      1000  /Users/agupta/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.6/lib/active_support/number_helper/number_to_delimited_converter.rb:15
      1000  /Users/agupta/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.6/lib/active_support/number_helper/number_to_rounded_converter.rb:33
      1000  /Users/agupta/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.6/lib/active_support/number_helper/number_to_rounded_converter.rb:37

From the couple of approaches we have

require 'rubygems'
require 'benchmark/ips'

# Current Implementation
def method_one
  a = 'foo'
  b = 'bar'
  a + '.' + b
end

# Appending
def method_two
  a = 'foo'
  b = 'bar'
  a << '.' << b
end

# Interpolation
def method_three
  a = 'foo'
  b = 'bar'
  a << ".#{b}"
end

Benchmark.ips do |x|
  x.config(:time => 5, :warmup => 2)
  x.report("method_one") { method_one }
  x.report("method_two") { method_two }
  x.report("method_three") { method_three }
  x.compare!
end


p "method_one output: #{method_one}"
p "method_two output: #{method_two}"
p "method_three output: #{method_three}"

Benchmark Output

agupta01sjl:check_active_support agupta$ ruby bm.rb 
Warming up --------------------------------------
          method_one    96.895k i/100ms
          method_two   103.509k i/100ms
        method_three    99.381k i/100ms
Calculating -------------------------------------
          method_one      2.290M (± 4.5%) i/s -     11.434M
          method_two      2.470M (± 6.0%) i/s -     12.318M
        method_three      2.487M (± 5.3%) i/s -     12.423M

Comparison:
        method_three:  2486823.3 i/s
          method_two:  2469837.1 i/s - same-ish: difference falls within error
          method_one:  2290160.7 i/s - same-ish: difference falls within error

"method_one output: foo.bar"
"method_two output: foo.bar"
"method_three output: foo.bar"

method_three (which is interpolation) is better but it would also create a string like .100 in this case. I am planning to go ahead with method_two

a << '.'.freeze << b[0, precision]

Wdyt @schneems ? The above removes all the extra 3000 strings and single allocation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird, this gives you an error

'.'.freeze << "what"
RuntimeError: can't modify frozen String

I'm surprised that putting another string in front doesn't. I guess this is because a << '.'.freeze gets evaluated first and returns a non-frozen string.

I would have done the more verbose

a << '.'.freeze
a << b[0, precision]

Your way is fine.

Since "s" is also not an input string and it doesn't look like it's used anywhere else we could do the same there

s = rounded_number.to_s('F'.freeze)
s << '0'.freeze * precision

That would save us two string allocations including getting rid of even more "0" strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@schneems Done. I ended up with

a << '.'.freeze
a << b[0, precision]

above was more readable.

@schneems
Copy link
Member

Please don't put frozen strings in constants, it is slower. Thanks for the work here! Very thorough PR.

@kaspth
Copy link
Contributor

kaspth commented Apr 11, 2016

r? @schneems

@rails-bot rails-bot assigned schneems and unassigned kaspth Apr 11, 2016
@ankit8898
Copy link
Contributor Author

@simi Shared won't help us much as all the helpers are not using . string. Plus as mentioned above Constant won't be a good option for it.

…ng to do the same string manipulation. This was we avoid the duplicate strings with freeze and append modifies existing string
@ankit8898
Copy link
Contributor Author

Summarizing

On the basis of above example which was iterating n times

number_to_delimited

We saved n extra '.' string initializations

1000 extra strings shaved for above example.

number_to_rounded

We saved n*3 extra '.' string initializations
We saved n '0' string initialization

4000 extra strings shaved for above example.

@schneems
Copy link
Member

Thanks for all the work!

@schneems schneems merged commit ecd5366 into rails:master Apr 12, 2016
@ankit8898 ankit8898 deleted the freezing-dot-in-delimiter-helper branch April 12, 2016 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants