Skip to content

Optimize ActiveSupport::NumericWithFormat#to_s #41801

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

Merged
merged 1 commit into from
Mar 31, 2021

Conversation

casperisfine
Copy link
Contributor

@casperisfine casperisfine commented Mar 30, 2021

case format when nil is very efficient because it end up calling NilClass === nil
which pretty much translates to nil.is_a?(NilClass). which is an alias for Object#==, so basically it end up doing nil == nil.

However format.nil? benefit from a dedicated op code, so it's significantly faster.

In this case Integer#to_s is much more often called without any arguments,
so it's worth optimizing for the most common case.

class Integer
  alias_method :faster_to_s, :to_s
end
require 'active_support/all'
require 'benchmark/ips'

module FasterNumericWithFormat
  def faster_to_s(format = nil, options = nil)
    if format.nil?
      return super()
    end

    case format
    when Integer, String
      super(format)
    when :phone
      ActiveSupport::NumberHelper.number_to_phone(self, options || {})
    when :currency
      ActiveSupport::NumberHelper.number_to_currency(self, options || {})
    when :percentage
      ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
    when :delimited
      ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
    when :rounded
      ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
    when :human
      ActiveSupport::NumberHelper.number_to_human(self, options || {})
    when :human_size
      ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
    when Symbol
      super()
    else
      super(format)
    end
  end
end

Integer.prepend(FasterNumericWithFormat)

Benchmark.ips do |x|
  x.report('orig no-arg') { 42.to_s }
  x.report('fast no-arg') { 42.faster_to_s }
  x.compare!
end

Benchmark.ips do |x|
  x.report('orig :human') { 42.to_s(:human) }
  x.report('fast :human') { 42.faster_to_s(:human) }
  x.compare!
end

Ruby 2.7.2

Warming up --------------------------------------
         orig no-arg   567.569k i/100ms
         fast no-arg   692.636k i/100ms
Calculating -------------------------------------
         orig no-arg      5.709M (± 1.3%) i/s -     28.946M in   5.070660s
         fast no-arg      6.892M (± 0.7%) i/s -     34.632M in   5.024961s

Comparison:
         fast no-arg:  6892287.7 i/s
         orig no-arg:  5709450.0 i/s - 1.21x  (± 0.00) slower

Warming up --------------------------------------
         orig :human   575.000  i/100ms
         fast :human   619.000  i/100ms
Calculating -------------------------------------
         orig :human      6.176k (± 1.6%) i/s -     31.050k in   5.028656s
         fast :human      6.179k (± 1.8%) i/s -     30.950k in   5.010372s

Comparison:
         fast :human:     6179.1 i/s
         orig :human:     6176.3 i/s - same-ish: difference falls within error

cc @rafaelfranca @kaspth in case you'd like to take a look. Otherwise I'll merge in a couple days.

`case format when nil` is very efficient because it end up calling `NilClass === nil`
which pretty much translates to `nil.is_a?(NilClass)`.

On the other hand `format.nil?` benefit from a dedicated op code, so it's quite faster.

In this case `Integer#to_s` is much more often called without any arguments,
so it's worth optimizing for the most common case.

```ruby
class Integer
  alias_method :faster_to_s, :to_s
end
require 'active_support/all'
require 'benchmark/ips'

module FasterNumericWithFormat
  def faster_to_s(format = nil, options = nil)
    if format.nil?
      return super()
    end

    case format
    when Integer, String
      super(format)
    when :phone
      ActiveSupport::NumberHelper.number_to_phone(self, options || {})
    when :currency
      ActiveSupport::NumberHelper.number_to_currency(self, options || {})
    when :percentage
      ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
    when :delimited
      ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
    when :rounded
      ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
    when :human
      ActiveSupport::NumberHelper.number_to_human(self, options || {})
    when :human_size
      ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
    when Symbol
      super()
    else
      super(format)
    end
  end
end

Integer.prepend(FasterNumericWithFormat)

Benchmark.ips do |x|
  x.report('orig no-arg') { 42.to_s }
  x.report('fast no-arg') { 42.faster_to_s }
  x.compare!
end

Benchmark.ips do |x|
  x.report('orig :human') { 42.to_s(:human) }
  x.report('fast :human') { 42.faster_to_s(:human) }
  x.compare!
end
```

Ruby 2.7.2
```
Warming up --------------------------------------
         orig no-arg   567.569k i/100ms
         fast no-arg   692.636k i/100ms
Calculating -------------------------------------
         orig no-arg      5.709M (± 1.3%) i/s -     28.946M in   5.070660s
         fast no-arg      6.892M (± 0.7%) i/s -     34.632M in   5.024961s

Comparison:
         fast no-arg:  6892287.7 i/s
         orig no-arg:  5709450.0 i/s - 1.21x  (± 0.00) slower

Warming up --------------------------------------
         orig :human   575.000  i/100ms
         fast :human   619.000  i/100ms
Calculating -------------------------------------
         orig :human      6.176k (± 1.6%) i/s -     31.050k in   5.028656s
         fast :human      6.179k (± 1.8%) i/s -     30.950k in   5.010372s

Comparison:
         fast :human:     6179.1 i/s
         orig :human:     6176.3 i/s - same-ish: difference falls within error

```
@byroot byroot merged commit d612542 into rails:main Mar 31, 2021
@casperisfine casperisfine deleted the optimize-numeric-to-s branch March 31, 2021 07:22
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.

4 participants