The letter A styled as Alchemists logo. lchemists
Published February 2, 2019 Updated September 16, 2026
Benchmarks Icon

Benchmarks

6.4.0

Benchmarks is a collection of Ruby micro benchmarks which can be cloned and run locally or used as an information point of reference. The various statistics on Ruby performance captured within this project may or may not surprise you.

Features

  • Uses Benchmark IPS to calculate CPU/speed results.

  • Each script is independently executable.

Requirements

Setup

To install, run:

git clone https://github.com/bkuhlmann/benchmarks.git
cd benchmarks
git checkout 6.4.0
bin/setup

Usage

All benchmark scripts are found within the scripts folder and you can run any benchmark using a relative or absolute file path. Example:

scripts/arrays/concatenation

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

a = %w[one two three]
b = %w[four five six]

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "#+" do
    a + b
  end

  benchmark.report "#+=" do
    duplicate = a.dup
    duplicate += b
  end

  benchmark.report "#concat" do
    a.dup.concat b
  end

  benchmark.report "#|" do
    a | b
  end

  benchmark.report "#<< + #flatten" do
    (a.dup << b).flatten
  end

  benchmark.report "splat + #flatten" do
    [a, *b].flatten
  end

  benchmark.report "multi-splat" do
    [*a, *b]
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                  #+     2.041M i/100ms
                 #+=   920.566k i/100ms
             #concat   763.628k i/100ms
                  #|   510.613k i/100ms
      #<< + #flatten   221.842k i/100ms
    splat + #flatten   241.441k i/100ms
         multi-splat     1.086M i/100ms
Calculating -------------------------------------
                  #+     22.794M (± 1.8%) i/s   (43.87 ns/i) -    114.323M in   5.017221s
                 #+=      9.402M (± 3.9%) i/s  (106.36 ns/i) -     47.869M in   5.099447s
             #concat      8.085M (± 2.7%) i/s  (123.69 ns/i) -     40.472M in   5.009540s
                  #|      5.224M (± 2.0%) i/s  (191.43 ns/i) -     26.552M in   5.085088s
      #<< + #flatten      2.372M (± 4.8%) i/s  (421.59 ns/i) -     11.979M in   5.062112s
    splat + #flatten      2.447M (± 5.0%) i/s  (408.69 ns/i) -     12.313M in   5.044622s
         multi-splat     11.437M (± 3.0%) i/s   (87.43 ns/i) -     57.535M in   5.034896s

Comparison:
                  #+: 22793834.0 i/s
         multi-splat: 11437362.6 i/s - 1.99x  slower
                 #+=:  9401673.8 i/s - 2.42x  slower
             #concat:  8085032.5 i/s - 2.82x  slower
                  #|:  5223801.3 i/s - 4.36x  slower
    splat + #flatten:  2446861.9 i/s - 9.32x  slower
      #<< + #flatten:  2371963.7 i/s - 9.61x  slower

scripts/arrays/search

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

list = %w[one two three four five six seven eight nine ten]
pattern = /t/

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("#grep") { list.grep pattern }
  benchmark.report("#select") { list.select { |value| value.match? pattern } }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
               #grep   175.145k i/100ms
             #select   236.877k i/100ms
Calculating -------------------------------------
               #grep      1.794M (± 4.6%) i/s  (557.47 ns/i) -      9.108M in   5.088368s
             #select      2.448M (± 3.8%) i/s  (408.47 ns/i) -     12.318M in   5.038546s

Comparison:
             #select:  2448151.9 i/s
               #grep:  1793833.0 i/s - 1.36x  slower

scripts/bindings

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

module Test
  def self.with_binding(end:) = binding.local_variable_get(:end)

  def self.with_pinning(end:) = {end:}[:end]
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Binding") { Test.with_binding end: 1 }
  benchmark.report("Pinning") { Test.with_pinning end: 1 }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
             Binding   709.213k i/100ms
             Pinning     1.663M i/100ms
Calculating -------------------------------------
             Binding      7.671M (± 5.5%) i/s  (130.37 ns/i) -     38.298M in   5.006932s
             Pinning     18.090M (± 6.4%) i/s   (55.28 ns/i) -     91.457M in   5.077294s

Comparison:
             Pinning: 18090003.1 i/s
             Binding:  7670731.9 i/s - 2.36x  slower

scripts/constants/equality

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

TEST = "test"

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Constant First") { TEST == "test" }
  benchmark.report("Value First") { "test" == TEST }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
      Constant First     4.277M i/100ms
         Value First     4.309M i/100ms
Calculating -------------------------------------
      Constant First     55.538M (± 0.2%) i/s   (18.01 ns/i) -    278.012M in   5.005822s
         Value First     55.303M (± 1.9%) i/s   (18.08 ns/i) -    280.063M in   5.066325s

Comparison:
      Constant First: 55537887.6 i/s
         Value First: 55302727.6 i/s - same-ish: difference falls within error

scripts/constants/lookup

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

CONSTANTS = Hash.new

module Constants
  1_000.times { |index| CONSTANTS["EXAMPLE_#{index}"] = const_set "EXAMPLE_#{index}", index }
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("#[]") { CONSTANTS["EXAMPLE_666"] }
  benchmark.report("Module.get (symbol)") { Constants.const_get :EXAMPLE_666 }
  benchmark.report("Module.get (string)") { Constants.const_get "EXAMPLE_666" }
  benchmark.report("Object.get") { Object.const_get "Constants::EXAMPLE_666" }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                 #[]     2.541M i/100ms
 Module.get (symbol)     3.355M i/100ms
 Module.get (string)     1.649M i/100ms
          Object.get   999.683k i/100ms
Calculating -------------------------------------
                 #[]     30.883M (± 0.4%) i/s   (32.38 ns/i) -    155.018M in   5.019618s
 Module.get (symbol)     40.391M (± 0.3%) i/s   (24.76 ns/i) -    204.660M in   5.067003s
 Module.get (string)     18.154M (± 0.6%) i/s   (55.08 ns/i) -     92.351M in   5.087198s
          Object.get     10.796M (± 0.5%) i/s   (92.63 ns/i) -     53.983M in   5.000510s

Comparison:
 Module.get (symbol): 40391128.3 i/s
                 #[]: 30882959.9 i/s - 1.31x  slower
 Module.get (string): 18154323.3 i/s - 2.22x  slower
          Object.get: 10795788.7 i/s - 3.74x  slower

scripts/delegates

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

require "delegate"
require "forwardable"

module Echo
  def self.call(message) = message
end

class ForwardExample
  def initialize operation
    @operation = operation
  end

  def call(...) = operation.call(...)

  private

  attr_reader :operation
end

class DelegateExample
  extend Forwardable

  delegate %i[call] => :operation

  def initialize operation
    @operation = operation
  end

  private

  attr_reader :operation
end

class SimpleExample < SimpleDelegator
end

class ClassExample < DelegateClass Echo
end

message = "A test."
forward_example = ForwardExample.new Echo
deletate_example = DelegateExample.new Echo
simple_example = SimpleExample.new Echo
class_example = ClassExample.new Echo

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Forward") { forward_example.call message }
  benchmark.report("Delegate") { deletate_example.call message }
  benchmark.report("Simple Delegator") { simple_example.call message }
  benchmark.report("Delegate Class") { class_example.call message }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
             Forward     2.154M i/100ms
            Delegate     1.730M i/100ms
    Simple Delegator   455.864k i/100ms
      Delegate Class   454.933k i/100ms
Calculating -------------------------------------
             Forward     25.482M (± 0.2%) i/s   (39.24 ns/i) -    129.237M in   5.071790s
            Delegate     21.359M (± 1.0%) i/s   (46.82 ns/i) -    107.246M in   5.021639s
    Simple Delegator      5.139M (± 3.5%) i/s  (194.60 ns/i) -     25.984M in   5.062787s
      Delegate Class      5.153M (± 4.2%) i/s  (194.04 ns/i) -     25.931M in   5.041101s

Comparison:
             Forward: 25481756.8 i/s
            Delegate: 21359007.4 i/s - 1.19x  slower
      Delegate Class:  5153461.2 i/s - 4.94x  slower
    Simple Delegator:  5138633.7 i/s - 4.96x  slower

scripts/functions/blocks

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

Example = Class.new do
  def echo_implicit text
    yield
    text
  end

  def echo_implicit_guard text
    yield if block_given?
    text
  end

  def echo_explicit text, &block
    yield block
    text
  end

  def echo_explicit_guard text, &block
    yield block if block
    text
  end
end

block_example = Example.new
lambda_example = -> text { text }
proc_example = proc { |text| text }

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "Block (implicit)" do
    block_example.echo_implicit("hi") { "test" }
  end

  benchmark.report "Block (implicit guard)" do
    block_example.echo_implicit_guard("hi") { "test" }
  end

  benchmark.report "Block (explicit)" do
    block_example.echo_explicit("hi") { "test" }
  end

  benchmark.report "Block (explicit guard)" do
    block_example.echo_explicit_guard("hi") { "test" }
  end

  benchmark.report "Lambda" do
    lambda_example.call "test"
  end

  benchmark.report "Proc" do
    proc_example.call "test"
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
    Block (implicit)     4.377M i/100ms
Block (implicit guard)
                         4.307M i/100ms
    Block (explicit)   765.699k i/100ms
Block (explicit guard)
                       733.839k i/100ms
              Lambda     2.591M i/100ms
                Proc     2.759M i/100ms
Calculating -------------------------------------
    Block (implicit)     55.054M (± 0.4%) i/s   (18.16 ns/i) -    275.761M in   5.009019s
Block (implicit guard)
                         55.424M (± 0.2%) i/s   (18.04 ns/i) -    279.958M in   5.051249s
    Block (explicit)      8.617M (±10.7%) i/s  (116.05 ns/i) -     42.879M in   5.028149s
Block (explicit guard)
                          8.640M (±11.0%) i/s  (115.74 ns/i) -     43.297M in   5.067570s
              Lambda     34.272M (± 0.2%) i/s   (29.18 ns/i) -    173.607M in   5.065559s
                Proc     34.493M (± 0.2%) i/s   (28.99 ns/i) -    173.797M in   5.038588s

Comparison:
Block (implicit guard): 55423617.9 i/s
    Block (implicit): 55053980.1 i/s - 1.01x  slower
                Proc: 34493418.9 i/s - 1.61x  slower
              Lambda: 34272211.4 i/s - 1.62x  slower
Block (explicit guard):  8640102.7 i/s - 6.41x  slower
    Block (explicit):  8616900.9 i/s - 6.43x  slower

scripts/functions/types

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

ProcFunction = proc { |message = "message"| message }
LambdaFunction = -> message = "benchmark" { message }

module ModuleFunction
  module_function

  def call(message = "benchmark") = message
end

class ClassFunction
  def initialize message = "benchmark"
    @message = message
  end

  def call = message

  private

  attr_reader :message
end

memoized_function = ClassFunction.new

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Proc") { ProcFunction.call }
  benchmark.report("Lambda") { LambdaFunction.call }
  benchmark.report("Module") { ModuleFunction.call }
  benchmark.report("Class (new)") { ClassFunction.new.call }
  benchmark.report("Class (memoized)") { memoized_function.call }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                Proc     2.215M i/100ms
              Lambda     2.228M i/100ms
              Module     4.451M i/100ms
         Class (new)     2.504M i/100ms
    Class (memoized)     4.430M i/100ms
Calculating -------------------------------------
                Proc     24.163M (± 1.4%) i/s   (41.38 ns/i) -    121.842M in   5.043510s
              Lambda     24.785M (± 1.2%) i/s   (40.35 ns/i) -    124.746M in   5.033908s
              Module     59.937M (± 0.9%) i/s   (16.68 ns/i) -    302.661M in   5.050088s
         Class (new)     28.137M (± 2.4%) i/s   (35.54 ns/i) -    142.739M in   5.075867s
    Class (memoized)     55.985M (± 0.9%) i/s   (17.86 ns/i) -    283.501M in   5.064328s

Comparison:
              Module: 59937028.6 i/s
    Class (memoized): 55984529.3 i/s - 1.07x  slower
         Class (new): 28137026.3 i/s - 2.13x  slower
              Lambda: 24785195.0 i/s - 2.42x  slower
                Proc: 24163375.2 i/s - 2.48x  slower

scripts/hashes/lookup

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

example = {a: 1, b: 2, c: 3}

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("#[]") { example[:b] }
  benchmark.report("#fetch") { example.fetch :b }
  benchmark.report("#fetch (default)") { example.fetch :b, "default" }
  benchmark.report("#fetch (block)") { example.fetch(:b) { "default" } }
  benchmark.report("#dig") { example.dig :b }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                 #[]     4.091M i/100ms
              #fetch     3.558M i/100ms
    #fetch (default)     3.537M i/100ms
      #fetch (block)     3.528M i/100ms
                #dig     3.896M i/100ms
Calculating -------------------------------------
                 #[]     51.599M (± 0.4%) i/s   (19.38 ns/i) -    261.851M in   5.074852s
              #fetch     42.667M (± 0.4%) i/s   (23.44 ns/i) -    213.486M in   5.003628s
    #fetch (default)     42.271M (± 0.6%) i/s   (23.66 ns/i) -    212.206M in   5.020335s
      #fetch (block)     42.642M (± 0.3%) i/s   (23.45 ns/i) -    215.197M in   5.046671s
                #dig     48.266M (± 0.6%) i/s   (20.72 ns/i) -    241.570M in   5.005108s

Comparison:
                 #[]: 51598749.0 i/s
                #dig: 48266387.2 i/s - 1.07x  slower
              #fetch: 42666996.7 i/s - 1.21x  slower
      #fetch (block): 42641658.5 i/s - 1.21x  slower
    #fetch (default): 42270877.3 i/s - 1.22x  slower

scripts/hashes/merge

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

extra = {b: 2}

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Splat") { {a: 1, **extra} }
  benchmark.report("Merge") { {a: 1}.merge extra }
  benchmark.report("Merge!") { {a: 1}.merge! extra }
  benchmark.report("Dup Merge!") { {a: 1}.dup.merge! extra }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
               Splat     1.338M i/100ms
               Merge   951.857k i/100ms
              Merge!     1.165M i/100ms
          Dup Merge!   641.603k i/100ms
Calculating -------------------------------------
               Splat     14.253M (± 5.1%) i/s   (70.16 ns/i) -     72.227M in   5.080706s
               Merge      9.126M (± 4.3%) i/s  (109.58 ns/i) -     45.689M in   5.015660s
              Merge!     12.741M (± 4.1%) i/s   (78.49 ns/i) -     64.086M in   5.038507s
          Dup Merge!      6.901M (± 4.9%) i/s  (144.90 ns/i) -     34.647M in   5.032388s

Comparison:
               Splat: 14252857.3 i/s
              Merge!: 12741219.4 i/s - 1.12x  slower
               Merge:  9125612.2 i/s - 1.56x  slower
          Dup Merge!:  6901442.6 i/s - 2.07x  slower

scripts/hashes/reduce

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

numbers = {
  one: 1,
  two: 2,
  three: 3,
  four: 4,
  five: 5,
  six: 6,
  seven: 7,
  eight: 8,
  nine: 9,
  ten: 10
}

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "Reduce" do
    numbers.reduce({}) { |collection, (key, value)| collection.merge! value => key }
  end

  benchmark.report "With Object" do
    numbers.each.with_object({}) { |(key, value), collection| collection[value] = key }
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
              Reduce    60.829k i/100ms
         With Object   112.893k i/100ms
Calculating -------------------------------------
              Reduce    613.452k (± 1.4%) i/s    (1.63 μs/i) -      3.102M in   5.058093s
         With Object      1.172M (± 4.9%) i/s  (853.10 ns/i) -      5.870M in   5.020591s

Comparison:
         With Object:  1172192.0 i/s
              Reduce:   613451.9 i/s - 1.91x  slower

scripts/loops

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

collection = (1..1_000).to_a
sum = 0

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "for" do
    for number in collection do
      sum += number
    end
  end

  benchmark.report "#each" do
    collection.each { |number| sum += number }
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                 for    18.540k i/100ms
               #each    20.918k i/100ms
Calculating -------------------------------------
                 for    188.671k (± 2.4%) i/s    (5.30 μs/i) -    945.540k in   5.014384s
               #each    210.097k (± 0.1%) i/s    (4.76 μs/i) -      1.067M in   5.077744s

Comparison:
               #each:   210097.2 i/s
                 for:   188670.5 i/s - 1.11x  slower

scripts/methods/define_method

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

require "forwardable"

Person = Class.new do
  def initialize first, last
    @first = first
    @last = last
  end

  def full_name
    "#{first} #{last}"
  end

  private

  attr_reader :first, :last
end

Example = Class.new Person do
  extend Forwardable

  define_method :unbound_full_name, Person.instance_method(:full_name)
  delegate %i[full_name] => :person

  def initialize first, last, person: Person.new(first, last)
    super first, last
    @person = person
  end

  def wrapped_full_name
    person.full_name
  end

  private

  attr_reader :first, :last, :person
end

example = Example.new "Jill", "Doe"

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Wrapped") { example.wrapped_full_name }
  benchmark.report("Defined") { example.unbound_full_name }
  benchmark.report("Delegated") { example.full_name }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
             Wrapped     1.513M i/100ms
             Defined     1.452M i/100ms
           Delegated     1.057M i/100ms
Calculating -------------------------------------
             Wrapped     16.392M (± 2.2%) i/s   (61.00 ns/i) -     83.190M in   5.077430s
             Defined     16.887M (± 3.4%) i/s   (59.22 ns/i) -     85.679M in   5.079576s
           Delegated     11.425M (± 2.3%) i/s   (87.53 ns/i) -     58.122M in   5.090109s

Comparison:
             Defined: 16887107.4 i/s
             Wrapped: 16392341.2 i/s - same-ish: difference falls within error
           Delegated: 11425005.5 i/s - 1.48x  slower

scripts/methods/method_proc

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

Example = Class.new do
  def initialize words
    @words = words
    @first_word = words.first
  end

  def direct_single
    say first_word
  end

  def direct_multiple
    words.each { |word| say word }
  end

  def proc_single
    method(:say).call first_word
  end

  def proc_multiple
    words.each { |word| method(:say).call word }
  end

  def method_to_proc_single
    first_word.then(&method(:say))
  end

  def method_to_proc_multiple
    words.each(&method(:say))
  end

  private

  attr_reader :words, :first_word

  def say phrase
    "You said: #{phrase}."
  end
end

example = Example.new %w[one two three]

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Direct (s)") { example.direct_single }
  benchmark.report("Direct (m)") { example.direct_multiple }
  benchmark.report("Proc (s)") { example.proc_single }
  benchmark.report("Proc (m)") { example.proc_multiple }
  benchmark.report("Method To Proc (s)") { example.method_to_proc_single }
  benchmark.report("Method To Proc (m)") { example.method_to_proc_multiple }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
          Direct (s)     1.639M i/100ms
          Direct (m)   593.203k i/100ms
            Proc (s)   796.563k i/100ms
            Proc (m)   286.583k i/100ms
  Method To Proc (s)   341.162k i/100ms
  Method To Proc (m)   219.307k i/100ms
Calculating -------------------------------------
          Direct (s)     17.826M (± 4.3%) i/s   (56.10 ns/i) -     90.123M in   5.065128s
          Direct (m)      6.082M (± 2.4%) i/s  (164.41 ns/i) -     30.847M in   5.074261s
            Proc (s)      9.032M (± 2.0%) i/s  (110.72 ns/i) -     45.404M in   5.028831s
            Proc (m)      2.991M (± 2.5%) i/s  (334.33 ns/i) -     15.189M in   5.081408s
  Method To Proc (s)      3.705M (± 7.5%) i/s  (269.92 ns/i) -     18.423M in   5.000012s
  Method To Proc (m)      2.244M (± 5.9%) i/s  (445.60 ns/i) -     11.185M in   5.000913s

Comparison:
          Direct (s): 17826000.7 i/s
            Proc (s):  9032188.7 i/s - 1.97x  slower
          Direct (m):  6082449.7 i/s - 2.93x  slower
  Method To Proc (s):  3704747.0 i/s - 4.81x  slower
            Proc (m):  2991088.4 i/s - 5.96x  slower
  Method To Proc (m):  2244146.0 i/s - 7.94x  slower

scripts/methods/scoped_send

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

module Example
  module_function

  def add(first = 1, second = 2) = first + second
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Public") { Example.public_send :add }
  benchmark.report("Private") { Example.__send__ :add }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
              Public     1.972M i/100ms
             Private     4.431M i/100ms
Calculating -------------------------------------
              Public     22.708M (± 1.7%) i/s   (44.04 ns/i) -    114.397M in   5.039230s
             Private     56.018M (±14.3%) i/s   (17.85 ns/i) -    265.872M in   5.054211s

Comparison:
             Private: 56018442.7 i/s
              Public: 22707682.3 i/s - 2.47x  slower

scripts/methods/send

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

module Static
  def self.call = rand > 0.5 ? one : two

  def self.one = 1

  def self.two = 2
end

module Dynamic
  def self.with_strings = public_send rand > 0.5 ? "one" : "two"

  def self.with_symbols = public_send rand > 0.5 ? :one : :two

  def self.one = 1

  def self.two = 2
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Static") { Static.call }
  benchmark.report("Dynamic (strings)") { Dynamic.with_strings }
  benchmark.report("Dynamic (symbols)") { Dynamic.with_symbols }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
              Static     2.450M i/100ms
   Dynamic (strings)   950.975k i/100ms
   Dynamic (symbols)     1.212M i/100ms
Calculating -------------------------------------
              Static     27.059M (± 3.1%) i/s   (36.96 ns/i) -    137.182M in   5.074840s
   Dynamic (strings)     11.243M (± 0.8%) i/s   (88.94 ns/i) -     57.058M in   5.075304s
   Dynamic (symbols)     15.294M (± 1.3%) i/s   (65.39 ns/i) -     77.579M in   5.073369s

Comparison:
              Static: 27058712.0 i/s
   Dynamic (symbols): 15293984.0 i/s - 1.77x  slower
   Dynamic (strings): 11243136.2 i/s - 2.41x  slower

scripts/numerics

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "bigdecimal"
  gem "benchmark-ips"
end

require "bigdecimal"

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Integer") { 1 + 0 }
  benchmark.report("Float") { 0.1 + 0 }
  benchmark.report("Rational") { (1 / 1r) + 0 }
  benchmark.report("BigDecimal") { BigDecimal("0.1") + 0 }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
             Integer     5.201M i/100ms
               Float     4.672M i/100ms
            Rational     1.511M i/100ms
          BigDecimal   392.886k i/100ms
Calculating -------------------------------------
             Integer     69.051M (± 2.1%) i/s   (14.48 ns/i) -    348.490M in   5.049610s
               Float     60.799M (± 0.2%) i/s   (16.45 ns/i) -    308.361M in   5.071791s
            Rational     16.517M (± 2.9%) i/s   (60.54 ns/i) -     83.094M in   5.035595s
          BigDecimal      4.020M (± 7.6%) i/s  (248.76 ns/i) -     20.037M in   5.011139s

Comparison:
             Integer: 69051130.2 i/s
               Float: 60799372.8 i/s - 1.14x  slower
            Rational: 16517265.0 i/s - 4.18x  slower
          BigDecimal:  4019908.7 i/s - 17.18x  slower

scripts/pattern_matching/multiple_type_check

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("include?") { [String, Symbol].include? :test.class }
  benchmark.report("in") { :test in String | Symbol }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
            include?     3.026M i/100ms
                  in     2.265M i/100ms
Calculating -------------------------------------
            include?     36.895M (± 1.6%) i/s   (27.10 ns/i) -    184.582M in   5.004292s
                  in     26.819M (± 1.1%) i/s   (37.29 ns/i) -    135.894M in   5.067758s

Comparison:
            include?: 36894579.7 i/s
                  in: 26819158.1 i/s - 1.38x  slower

scripts/pattern_matching/single_type_check

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("is_a?") { :test.is_a? Symbol }
  benchmark.report("in") { :test in Symbol }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
               is_a?     5.043M i/100ms
                  in     3.279M i/100ms
Calculating -------------------------------------
               is_a?     68.582M (± 0.4%) i/s   (14.58 ns/i) -    342.936M in   5.000442s
                  in     38.724M (± 1.2%) i/s   (25.82 ns/i) -    196.724M in   5.080921s

Comparison:
               is_a?: 68582153.6 i/s
                  in: 38724445.4 i/s - 1.77x  slower

scripts/refinements/import

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

module Import
  def dud? = true
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "With" do
    Module.new { refine(String) { import_methods Import } }
  end

  benchmark.report "Without" do
    Module.new { def dud? = true }
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                With   361.000 i/100ms
             Without   265.247k i/100ms
Calculating -------------------------------------
                With      5.399k (±193.0%) i/s  (185.22 μs/i) -     10.469k in   5.385643s
             Without      2.616M (± 6.0%) i/s  (382.20 ns/i) -     13.262M in   5.090719s

Comparison:
             Without:  2616438.1 i/s
                With:     5399.0 i/s - 484.62x  slower

scripts/refinements/initialize

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

module Refines
  refine String do
    def dud? = true
  end
end

class With
  using Refines

  def initialize value = "demo"
    @value = value
  end
end

class Without
  def initialize value = "demo"
    @value = value
  end
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("With") { With.new }
  benchmark.report("Without") { Without.new }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                With     2.610M i/100ms
             Without     2.478M i/100ms
Calculating -------------------------------------
                With     30.900M (± 2.1%) i/s   (32.36 ns/i) -    156.573M in   5.069302s
             Without     30.128M (± 2.8%) i/s   (33.19 ns/i) -    151.164M in   5.021253s

Comparison:
                With: 30899772.3 i/s
             Without: 30127903.2 i/s - same-ish: difference falls within error

scripts/refinements/message

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

module Refines
  refine String do
    def dud? = true
  end
end

module With
  using Refines

  def self.call(value) = value.dud?
end

module Without
  def self.call(value) = value
end

value = "demo"

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("With") { With.call value }
  benchmark.report("Without") { Without.call value }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                With     3.057M i/100ms
             Without     4.933M i/100ms
Calculating -------------------------------------
                With     36.283M (± 0.5%) i/s   (27.56 ns/i) -    183.444M in   5.056028s
             Without     64.881M (± 2.9%) i/s   (15.41 ns/i) -    325.610M in   5.023843s

Comparison:
             Without: 64881052.6 i/s
                With: 36283337.8 i/s - 1.79x  slower

scripts/refinements/refine

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "With" do
    Module.new do
      refine String do
        def dud? = true
      end
    end
  end

  benchmark.report "Without" do
    Module.new do
      def dud? = true
    end
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                With   364.000 i/100ms
             Without   267.270k i/100ms
Calculating -------------------------------------
                With      5.265k (±190.8%) i/s  (189.93 μs/i) -     10.920k in   5.273126s
             Without      2.578M (± 2.1%) i/s  (387.97 ns/i) -     13.096M in   5.083279s

Comparison:
             Without:  2577506.5 i/s
                With:     5265.0 i/s - 489.55x  slower

scripts/strings/concatenation

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

one = "One"
two = "Two"
three = "Three"
four = "Four"
five = "Five"
six = "Six"
seven = "Seven"
eight = "Eight"
nine = "Nine"
ten = "Ten"

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "Implicit (<)" do
    "One" "Two"
  end

  benchmark.report "Implicit (>)" do
    "One" "Two" "Three" "Four" "Five" "Six" "Seven" "Eight" "Nine" "Ten"
  end

  benchmark.report "Interpolation (<)" do
    "#{one} #{two}"
  end

  benchmark.report "Interpolation (>)" do
    "#{one} #{two} #{three} #{four} #{five} #{six} #{seven} #{eight} #{nine} #{ten}"
  end

  benchmark.report "#+ (<)" do
    one + " " + two
  end

  benchmark.report "#+ (>)" do
    one + " " + two + " " + three + " " + four + " " + five + " " + six + " " + seven + " " +
    eight + " " + nine + " " + ten
  end

  # Mutation.
  benchmark.report "#concat (<)" do
    one.dup.concat two
  end

  # Mutation.
  benchmark.report "#concat (>)" do
    one.dup.concat two, three, four, five, six, seven, eight, nine, ten
  end

  # Mutation.
  benchmark.report "#<< (<)" do
    one.dup << two
  end

  # Mutation.
  benchmark.report "#<< (>)" do
    one.dup << two << three << four << five << six << seven << eight << nine << ten
  end

  benchmark.report "Array#join (<)" do
    [one, two].join " "
  end

  benchmark.report "Array#join (>)" do
    [one, two, three, four, five, six, seven, eight, nine, ten].join " "
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
        Implicit (<)     4.612M i/100ms
        Implicit (>)     5.156M i/100ms
   Interpolation (<)     1.681M i/100ms
   Interpolation (>)   472.381k i/100ms
              #+ (<)     1.279M i/100ms
              #+ (>)   161.656k i/100ms
         #concat (<)   524.718k i/100ms
         #concat (>)   228.594k i/100ms
             #<< (<)   576.251k i/100ms
             #<< (>)   302.123k i/100ms
      Array#join (<)     1.129M i/100ms
      Array#join (>)   397.504k i/100ms
Calculating -------------------------------------
        Implicit (<)     71.067M (± 0.1%) i/s   (14.07 ns/i) -    359.707M in   5.061526s
        Implicit (>)     71.055M (± 0.2%) i/s   (14.07 ns/i) -    355.735M in   5.006479s
   Interpolation (<)     18.556M (± 4.3%) i/s   (53.89 ns/i) -     94.139M in   5.083215s
   Interpolation (>)      5.022M (± 2.0%) i/s  (199.11 ns/i) -     25.509M in   5.080934s
              #+ (<)     13.447M (± 2.3%) i/s   (74.37 ns/i) -     67.761M in   5.041852s
              #+ (>)      1.615M (± 1.0%) i/s  (619.16 ns/i) -      8.083M in   5.005058s
         #concat (<)      5.668M (± 7.9%) i/s  (176.44 ns/i) -     28.335M in   5.027373s
         #concat (>)      2.535M (± 7.1%) i/s  (394.52 ns/i) -     12.801M in   5.072887s
             #<< (<)      6.124M (± 5.1%) i/s  (163.30 ns/i) -     31.118M in   5.093028s
             #<< (>)      3.316M (±11.8%) i/s  (301.60 ns/i) -     16.617M in   5.081439s
      Array#join (<)     11.789M (± 3.1%) i/s   (84.82 ns/i) -     59.820M in   5.078832s
      Array#join (>)      4.197M (± 1.6%) i/s  (238.26 ns/i) -     21.068M in   5.020997s

Comparison:
        Implicit (<): 71067132.6 i/s
        Implicit (>): 71055061.6 i/s - same-ish: difference falls within error
   Interpolation (<): 18555791.4 i/s - 3.83x  slower
              #+ (<): 13447051.5 i/s - 5.28x  slower
      Array#join (<): 11789148.3 i/s - 6.03x  slower
             #<< (<):  6123868.9 i/s - 11.60x  slower
         #concat (<):  5667784.5 i/s - 12.54x  slower
   Interpolation (>):  5022411.6 i/s - 14.15x  slower
      Array#join (>):  4197045.1 i/s - 16.93x  slower
             #<< (>):  3315687.6 i/s - 21.43x  slower
         #concat (>):  2534713.1 i/s - 28.04x  slower
              #+ (>):  1615100.5 i/s - 44.00x  slower

scripts/strings/matching

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

require "securerandom"

word = SecureRandom.alphanumeric 100
string_matcher = "a"
regex_matcher = /\Aa/

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("#match?") { word.match? regex_matcher }
  benchmark.report("#=~") { word =~ regex_matcher }
  benchmark.report("#start_with? (String)") { word.start_with? string_matcher }
  benchmark.report("#start_with? (Regex)") { word.start_with? regex_matcher }
  benchmark.report("#end_with?") { word.end_with? string_matcher }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
             #match?     2.526M i/100ms
                 #=~     1.339M i/100ms
#start_with? (String)
                         3.002M i/100ms
#start_with? (Regex)     1.048M i/100ms
          #end_with?     3.031M i/100ms
Calculating -------------------------------------
             #match?     28.780M (± 0.1%) i/s   (34.75 ns/i) -    143.989M in   5.003006s
                 #=~     15.506M (± 0.3%) i/s   (64.49 ns/i) -     77.686M in   5.010257s
#start_with? (String)
                         34.909M (± 0.1%) i/s   (28.65 ns/i) -    177.099M in   5.073225s
#start_with? (Regex)     11.312M (± 2.4%) i/s   (88.41 ns/i) -     56.611M in   5.007621s
          #end_with?     35.429M (± 0.1%) i/s   (28.23 ns/i) -    178.822M in   5.047302s

Comparison:
          #end_with?: 35429317.7 i/s
#start_with? (String): 34908613.0 i/s - 1.01x  slower
             #match?: 28780485.2 i/s - 1.23x  slower
                 #=~: 15505538.4 i/s - 2.28x  slower
#start_with? (Regex): 11311568.1 i/s - 3.13x  slower

scripts/strings/split

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

require "securerandom"

words = Array.new(100_000) { SecureRandom.alphanumeric 10 }
delimiter = " "
text = words.join delimiter
pattern = /\Aa/

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "Without Block" do
    text.split(delimiter).grep(pattern)
  end

  benchmark.report "With Block" do
    selections = []
    text.split(delimiter) { |word| selections << word if word.match? pattern }
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
       Without Block    14.000 i/100ms
          With Block    14.000 i/100ms
Calculating -------------------------------------
       Without Block    146.507 (± 0.7%) i/s    (6.83 ms/i) -    742.000 in   5.064948s
          With Block    147.138 (± 0.7%) i/s    (6.80 ms/i) -    742.000 in   5.043408s

Comparison:
          With Block:      147.1 i/s
       Without Block:      146.5 i/s - same-ish: difference falls within error

scripts/strings/substrings

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

example = "example"

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("#sub (string)") { example.sub "x", "b" }
  benchmark.report("#sub (regex)") { example.sub(/x/, "b") }
  benchmark.report("#gsub (string)") { example.gsub "x", "b" }
  benchmark.report("#gsub (regex)") { example.gsub(/x/, "b") }
  benchmark.report("#tr") { example.tr "x", "b" }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
       #sub (string)   773.049k i/100ms
        #sub (regex)   543.605k i/100ms
      #gsub (string)   529.823k i/100ms
       #gsub (regex)   303.931k i/100ms
                 #tr     1.303M i/100ms
Calculating -------------------------------------
       #sub (string)      8.034M (± 3.1%) i/s  (124.47 ns/i) -     40.199M in   5.008197s
        #sub (regex)      5.754M (± 2.3%) i/s  (173.81 ns/i) -     28.811M in   5.010357s
      #gsub (string)      5.589M (± 1.4%) i/s  (178.93 ns/i) -     28.081M in   5.025528s
       #gsub (regex)      3.195M (± 1.7%) i/s  (313.01 ns/i) -     16.108M in   5.043536s
                 #tr     14.137M (± 1.7%) i/s   (70.74 ns/i) -     71.667M in   5.071218s

Comparison:
                 #tr: 14136555.5 i/s
       #sub (string):  8033950.8 i/s - 1.76x  slower
        #sub (regex):  5753543.8 i/s - 2.46x  slower
      #gsub (string):  5588726.1 i/s - 2.53x  slower
       #gsub (regex):  3194812.0 i/s - 4.42x  slower

scripts/thens

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report "standard" do
    one, two = "one two".split
    "#{one} + #{two} = #{one + two}"
  end

  benchmark.report "then" do
    "one two".split.then { |one, two| "#{one} + #{two} = #{one + two}" }
  end

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
            standard   518.819k i/100ms
                then   473.521k i/100ms
Calculating -------------------------------------
            standard      5.357M (± 1.6%) i/s  (186.69 ns/i) -     26.979M in   5.037822s
                then      4.990M (± 2.7%) i/s  (200.40 ns/i) -     25.097M in   5.032935s

Comparison:
            standard:  5356571.3 i/s
                then:  4990075.4 i/s - 1.07x  slower

scripts/values/inheritance

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
end

PlotStruct = Struct.new :x, :y

class PlotSubclass < Struct.new :x, :y
end

struct = -> { PlotStruct[x: 1, y: 2] }
subclass = -> { PlotSubclass[x: 1, y: 2] }

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Struct") { struct.call }
  benchmark.report("Subclass") { subclass.call }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
              Struct   558.663k i/100ms
            Subclass   529.001k i/100ms
Calculating -------------------------------------
              Struct      5.979M (± 2.0%) i/s  (167.25 ns/i) -     30.168M in   5.047641s
            Subclass      5.465M (± 2.1%) i/s  (182.99 ns/i) -     27.508M in   5.035777s

Comparison:
              Struct:  5979019.4 i/s
            Subclass:  5464846.9 i/s - 1.09x  slower

scripts/values/initialization

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
  gem "dry-struct"
  gem "ostruct"
end

Warning[:performance] = false

DataDefault = Data.define :a, :b, :c, :d, :e

DataCustom = Data.define :a, :b, :c, :d, :e do
  def initialize a: 1, b: 2, c: 3, d: 4, e: 5
    super
  end
end

StructDefault = Struct.new :a, :b, :c, :d, :e

StructCustom = Struct.new :a, :b, :c, :d, :e do
  def initialize a: 1, b: 2, c: 3, d: 4, e: 5
    super
  end
end

module Types
  include Dry.Types
end

DryExample = Class.new Dry::Struct do
  attribute :a, Types::Strict::Integer
  attribute :b, Types::Strict::Integer
  attribute :c, Types::Strict::Integer
  attribute :d, Types::Strict::Integer
  attribute :e, Types::Strict::Integer
end

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Data (positional)") { DataDefault[1, 2, 3, 4, 5] }
  benchmark.report("Data (keyword)") { DataDefault[a: 1, b: 2, c: 3, d: 4, e: 5] }
  benchmark.report("Data (custom)") { DataCustom.new }
  benchmark.report("Struct (positional)") { StructDefault[1, 2, 3, 4, 5] }
  benchmark.report("Struct (keyword)") { StructDefault[a: 1, b: 2, c: 3, d: 4, e: 5] }
  benchmark.report("Struct (custom)") { StructCustom.new }
  benchmark.report("OpenStruct") { OpenStruct.new a: 1, b: 2, c: 3, d: 4, e: 5 }
  benchmark.report("Dry Struct") { DryExample[a: 1, b: 2, c: 3, d: 4, e: 5] }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
   Data (positional)   347.645k i/100ms
      Data (keyword)   362.798k i/100ms
       Data (custom)   305.941k i/100ms
 Struct (positional)     1.134M i/100ms
    Struct (keyword)   352.578k i/100ms
     Struct (custom)   403.416k i/100ms
          OpenStruct     8.106k i/100ms
          Dry Struct   127.913k i/100ms
Calculating -------------------------------------
   Data (positional)      3.695M (± 2.2%) i/s  (270.64 ns/i) -     18.773M in   5.083364s
      Data (keyword)      3.790M (± 2.7%) i/s  (263.83 ns/i) -     19.228M in   5.076899s
       Data (custom)      3.181M (± 4.4%) i/s  (314.39 ns/i) -     15.909M in   5.011290s
 Struct (positional)     12.671M (± 3.2%) i/s   (78.92 ns/i) -     63.494M in   5.016454s
    Struct (keyword)      3.691M (± 2.6%) i/s  (270.94 ns/i) -     18.687M in   5.066576s
     Struct (custom)      4.115M (± 3.9%) i/s  (243.03 ns/i) -     20.574M in   5.008155s
          OpenStruct     98.977k (±23.9%) i/s   (10.10 μs/i) -    478.254k in   5.053046s
          Dry Struct      1.305M (± 4.8%) i/s  (766.10 ns/i) -      6.524M in   5.009218s

Comparison:
 Struct (positional): 12670762.4 i/s
     Struct (custom):  4114733.4 i/s - 3.08x  slower
      Data (keyword):  3790379.2 i/s - 3.34x  slower
   Data (positional):  3694910.1 i/s - 3.43x  slower
    Struct (keyword):  3690814.1 i/s - 3.43x  slower
       Data (custom):  3180762.4 i/s - 3.98x  slower
          Dry Struct:  1305310.7 i/s - 9.71x  slower
          OpenStruct:    98977.4 i/s - 128.02x  slower

ℹ️ `Data` is fastest when members are small (like three or less) but performance degrades when more members are added (like five or more). This is because `Data` always initializes with a `Hash` which is not the case with a `Struct`. Additionally, passing keyword arguments to/from Ruby to Ruby is optimized while to/from Ruby/C is not.

scripts/values/reading

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
  gem "dry-struct"
  gem "ostruct"
end

DataExample = Data.define :to, :from
StructExample = Struct.new :to, :from

module Types
  include Dry.Types
end

DryExample = Class.new Dry::Struct do
  attribute :to, Types::Strict::String
  attribute :from, Types::Strict::String
end

data = DataExample[to: "Rick", from: "Morty"]
struct = StructExample[to: "Rick", from: "Morty"]
open_struct = OpenStruct.new to: "Rick", from: "Morty"
dry_struct = DryExample[to: "Rick", from: "Morty"]

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Data") { data.to }
  benchmark.report("Struct") { struct.to }
  benchmark.report("OpenStruct") { open_struct.to }
  benchmark.report("Dry Struct") { dry_struct.to }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                Data     4.996M i/100ms
              Struct     4.956M i/100ms
          OpenStruct     2.695M i/100ms
          Dry Struct     3.801M i/100ms
Calculating -------------------------------------
                Data     65.623M (± 3.1%) i/s   (15.24 ns/i) -    329.765M in   5.031036s
              Struct     65.816M (± 0.5%) i/s   (15.19 ns/i) -    332.049M in   5.045233s
          OpenStruct     31.489M (± 0.1%) i/s   (31.76 ns/i) -    159.018M in   5.049951s
          Dry Struct     46.498M (± 0.2%) i/s   (21.51 ns/i) -    235.678M in   5.068601s

Comparison:
              Struct: 65816109.5 i/s
                Data: 65623017.2 i/s - same-ish: difference falls within error
          Dry Struct: 46497703.7 i/s - 1.42x  slower
          OpenStruct: 31489086.6 i/s - 2.09x  slower

scripts/values/writing

Source
#! /usr/bin/env ruby
# frozen_string_literal: true

require "bundler/inline"

gemfile true do
  source "https://rubygems.org"

  gem "benchmark-ips"
  gem "ostruct"
end

DataExample = Data.define :to, :from
StructExample = Struct.new :to, :from

data = DataExample[to: "Rick", from: "Morty"]
struct = StructExample[to: "Rick", from: "Morty"]
open_struct = OpenStruct.new to: "Rick", from: "Morty"

Benchmark.ips do |benchmark|
  benchmark.config time: 5, warmup: 2

  benchmark.report("Data") { data.with from: "Summer" }
  benchmark.report("Struct") { struct.from = "Summer" }
  benchmark.report("OpenStruct") { open_struct.from = "Summer" }

  benchmark.compare!
end

Benchmark

ruby 4.0.0 (2025-12-25 revision 553f1675f3) +YJIT +PRISM [arm64-darwin25.2.0]
Warming up --------------------------------------
                Data   362.337k i/100ms
              Struct     4.119M i/100ms
          OpenStruct     2.193M i/100ms
Calculating -------------------------------------
                Data      3.656M (± 4.5%) i/s  (273.55 ns/i) -     18.479M in   5.065731s
              Struct     51.295M (± 0.2%) i/s   (19.50 ns/i) -    259.477M in   5.058586s
          OpenStruct     25.689M (± 1.2%) i/s   (38.93 ns/i) -    129.410M in   5.038280s

Comparison:
              Struct: 51294551.5 i/s
          OpenStruct: 25689237.2 i/s - 2.00x  slower
                Data:  3655574.7 i/s - 14.03x  slower

Development

To contribute, run:

git clone https://github.com/bkuhlmann/benchmarks.git
cd benchmarks
bin/setup

To render documentation for all benchmark scripts, run:

bin/render

This is the same script used to update the documentation within this README.

Tests

To test, run:

bin/rake

Credits