From 7cfc4ee676ed7d248f35c4bb038a1f16efd2a97f Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 5 Dec 2023 12:42:40 +0100 Subject: [PATCH] Optimize `Time.at_with_coercion` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ``` --- .../core_ext/time/calculations.rb | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index a117573bbe6..839d6ddb969 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -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