Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Speedup ActiveModel::Type::Integer#serialize by up to 10x
- Add a fast path for Integer. - Avoid using a regexp to check for numeric strings. - Directly compare with min/max rather than use a Range. - Inline some methods. ``` == str == ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23] Warming up -------------------------------------- int.serialize 267.196k i/100ms opt_int.serialize 2.420M i/100ms Calculating ------------------------------------- int.serialize 2.815M (± 1.5%) i/s (355.27 ns/i) - 14.161M in 5.032196s opt_int.serialize 28.564M (± 1.1%) i/s (35.01 ns/i) - 145.221M in 5.084744s Comparison: int.serialize: 2814789.7 i/s opt_int.serialize: 28563531.6 i/s - 10.15x faster == int == ruby 3.3.4 (2024-07-09 revision be1089c8ec) +YJIT [arm64-darwin23] Warming up -------------------------------------- int.serialize 1.084M i/100ms opt_int.serialize 3.926M i/100ms Calculating ------------------------------------- int.serialize 15.580M (± 1.3%) i/s (64.18 ns/i) - 78.061M in 5.011141s opt_int.serialize 53.040M (± 1.5%) i/s (18.85 ns/i) - 266.991M in 5.034991s Comparison: int.serialize: 15580113.2 i/s opt_int.serialize: 53039540.2 i/s - 3.40x faster ``` ```ruby require "bundler/inline" gemfile(true) do source "https://rubygems.org" gem "rails", "8.0.0.rc1" gem "sqlite3" gem "benchmark-ips" end require "active_record" require "benchmark/ips" require "logger" module ActiveModel module Type class OptInteger < Value include Helpers::Numeric # Column storage size in bytes. # 4 bytes means an integer as opposed to smallint etc. DEFAULT_LIMIT = 4 def initialize(**) super @limit ||= DEFAULT_LIMIT @max = 1 << (@limit * 8 - 1) # 8 bits per byte with one bit for sign @min = -@max end def type :integer end def deserialize(value) return if value.blank? value.to_i end def serialize(value) case value when ::Integer # noop when ::String int = value.to_i if int.zero? && value != "0" return if non_numeric_string?(value) end value = int else value = super end if value && (@max < value || @min > value) raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{limit} bytes" end value end def serialize_cast_value(value) # :nodoc: if value && (@max < value || @min > value) raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{limit} bytes" end value end def serializable?(value) cast_value = cast(value) return true unless cast_value return true unless (@max < value || @min > value) yield cast_value if block_given? false end private def cast_value(value) value.to_i rescue nil end end end end int = ActiveModel::Type::Integer.new opt_int = ActiveModel::Type::OptInteger.new puts "== str ==" Benchmark.ips do |x| x.report("int.serialize") { int.serialize("42") } x.report("opt_int.serialize") { opt_int.serialize("42") } x.compare!(order: :baseline) end puts "== int ==" Benchmark.ips do |x| x.report("int.serialize") { int.serialize(42) } x.report("opt_int.serialize") { opt_int.serialize(42) } x.compare!(order: :baseline) end ```
- Loading branch information