Optimize `Time.at_with_coercion`

By using `ruby2_keyword` style delegation we we can avoid a
few allocations and some extra checks.

```
$ ruby --yjit /tmp/bench-as-time-at.rb
ruby 3.2.2 (2023-03-30 revision e51014f9c0) +YJIT [arm64-darwin22]
=== Complex call ====
Warming up --------------------------------------
        Time.without   320.514k i/100ms
           Time.with    68.433k i/100ms
       Time.opt_with   167.532k i/100ms
Calculating -------------------------------------
        Time.without      3.781M (± 4.8%) i/s -     18.910M in   5.014574s
           Time.with      1.586M (± 3.5%) i/s -      7.938M in   5.010525s
       Time.opt_with      2.003M (± 2.4%) i/s -     10.052M in   5.021309s

Comparison:
        Time.without:  3781330.9 i/s
       Time.opt_with:  2003025.9 i/s - 1.89x  slower
           Time.with:  1586289.9 i/s - 2.38x  slower

Time.without: 2.003 alloc/iter
Time.with: 9.002 alloc/iter
Time.opt_with: 7.002 alloc/iter

=== Simple call ====
Warming up --------------------------------------
        Time.without   749.097k i/100ms
           Time.with   342.855k i/100ms
       Time.opt_with   416.063k i/100ms
Calculating -------------------------------------
        Time.without      9.289M (± 3.4%) i/s -     46.444M in   5.005361s
           Time.with      3.601M (± 2.1%) i/s -     18.171M in   5.048794s
       Time.opt_with      4.373M (± 8.1%) i/s -     22.051M in   5.084967s

Comparison:
        Time.without:  9289271.2 i/s
       Time.opt_with:  4373226.2 i/s - 2.12x  slower
           Time.with:  3600733.6 i/s - 2.58x  slower

Time.without: 1.002 alloc/iter
Time.with: 3.001 alloc/iter
Time.opt_with: 3.002 alloc/iter
```

```ruby
require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'activesupport', require: 'active_support/all', github: 'rails/rails'
  gem 'benchmark-ips'
end

class Time
  class << self
    def opt_at_with_coercion(time_or_number, *args)
      if args.empty?
        if time_or_number.is_a?(ActiveSupport::TimeWithZone)
          at_without_coercion(time_or_number.to_r).getlocal
        elsif time_or_number.is_a?(DateTime)
          at_without_coercion(time_or_number.to_f).getlocal
        else
          at_without_coercion(time_or_number)
        end
      else
        at_without_coercion(time_or_number, *args)
      end
    end
    ruby2_keywords :opt_at_with_coercion
  end
end

puts RUBY_DESCRIPTION

puts "=== Complex call ===="
Benchmark.ips do |x|
  x.report("Time.without") do
    ::Time.at_without_coercion(223423423, 32423423, :nanosecond, in: "UTC")
  end

  x.report("Time.with") do
    ::Time.at_with_coercion(223423423, 32423423, :nanosecond, in: "UTC")
  end

  x.report("Time.opt_with") do
    ::Time.opt_at_with_coercion(223423423, 32423423, :nanosecond, in: "UTC")
  end

  x.compare!(order: :baseline)
end

def measure_allocs(title, iterations: 1_000)
  before = GC.stat(:total_allocated_objects)
  iterations.times do
    yield
  end
  allocs = GC.stat(:total_allocated_objects) - before
  puts "#{title}: #{allocs.to_f / iterations} alloc/iter"
end

measure_allocs("Time.without") do
  ::Time.at_without_coercion(223423423, 32423423, :nanosecond, in: "UTC")
end

measure_allocs("Time.with") do
  ::Time.at_with_coercion(223423423, 32423423, :nanosecond, in: "UTC")
end

measure_allocs("Time.opt_with") do
  ::Time.opt_at_with_coercion(223423423, 32423423, :nanosecond, in: "UTC")
end

puts "=== Simple call ===="
Benchmark.ips do |x|
  x.report("Time.without") do
    ::Time.at_without_coercion(223423423)
  end

  x.report("Time.with") do
    ::Time.at_with_coercion(223423423)
  end

  x.report("Time.opt_with") do
    ::Time.opt_at_with_coercion(223423423)
  end

  x.compare!(order: :baseline)
end

def measure_allocs(title, iterations: 1_000)
  before = GC.stat(:total_allocated_objects)
  iterations.times do
    yield
  end
  allocs = GC.stat(:total_allocated_objects) - before
  puts "#{title}: #{allocs.to_f / iterations} alloc/iter"
end

measure_allocs("Time.without") do
  ::Time.at_without_coercion(223423423)
end

measure_allocs("Time.with") do
  ::Time.at_with_coercion(223423423)
end

measure_allocs("Time.opt_with") do
  ::Time.opt_at_with_coercion(223423423, 32423423)
end
```
This commit is contained in:
Jean Boussier 2023-12-05 12:42:40 +01:00
parent 1c8be9c67a
commit 7cfc4ee676
1 changed files with 11 additions and 11 deletions

View File

@ -42,20 +42,20 @@ class Time
# Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
# instances can be used when called with a single argument
def at_with_coercion(*args, **kwargs)
return at_without_coercion(*args, **kwargs) if args.size != 1 || !kwargs.empty?
# Time.at can be called with a time or numerical value
time_or_number = args.first
if time_or_number.is_a?(ActiveSupport::TimeWithZone)
at_without_coercion(time_or_number.to_r).getlocal
elsif time_or_number.is_a?(DateTime)
at_without_coercion(time_or_number.to_f).getlocal
def at_with_coercion(time_or_number, *args)
if args.empty?
if time_or_number.is_a?(ActiveSupport::TimeWithZone)
at_without_coercion(time_or_number.to_r).getlocal
elsif time_or_number.is_a?(DateTime)
at_without_coercion(time_or_number.to_f).getlocal
else
at_without_coercion(time_or_number)
end
else
at_without_coercion(time_or_number)
at_without_coercion(time_or_number, *args)
end
end
ruby2_keywords :at_with_coercion
alias_method :at_without_coercion, :at
alias_method :at, :at_with_coercion