In Erik Michaels-Ober's great talk, 'Writing Fast Ruby': Video @ Baruco 2014, Slide, he presented us with many idioms that lead to faster running Ruby code. He inspired me to document these to let more people know. I try to link to real commits so people can see that this can really have benefits in the real world. This does not mean you can always blindly replace one with another. It depends on the context (e.g. gsub versus tr). Friendly reminder: Use with caution!
Each idiom has a corresponding code example that resides in code.
All results listed in README.md are running with Ruby 4.0.0 on macOS 15.6.1. Machine information: MacBook Pro (14-inch, Nov 2024), Apple M4 Pro, 48 GB RAM. Your results may vary, but you get the idea. : )
You can checkout the GitHub Actions build for these benchmark results ran against different Ruby implementations.
Let's write faster code, together! <3
Checkout the fasterer project - it's a static analysis that checks speed idioms written in this repo.
Use benchmark-ips (2.0+).
require "benchmark/ips"
def fast
end
def slow
end
Benchmark.ips do |x|
x.report("fast code description") { fast }
x.report("slow code description") { slow }
x.compare!
endattr_accessor vs getter and setter code
https://www.omniref.com/ruby/2.2.0/files/method.h?#annotation=4081781&line=47
$ ruby -v code/general/attr-accessor-vs-getter-and-setter.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
getter_and_setter 1.122M i/100ms
attr_accessor 1.303M i/100ms
Calculating -------------------------------------
getter_and_setter 10.999M (± 2.1%) i/s (90.92 ns/i) - 56.083M in 5.101077s
attr_accessor 12.517M (± 1.5%) i/s (79.89 ns/i) - 63.841M in 5.101477s
Comparison:
attr_accessor: 12517008.0 i/s
getter_and_setter: 10999275.5 i/s - 1.14x slower
begin...rescue vs respond_to? for Control Flow code
$ ruby -v code/general/begin-rescue-vs-respond-to.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
begin...rescue 230.601k i/100ms
respond_to? 1.915M i/100ms
Calculating -------------------------------------
begin...rescue 2.390M (± 0.9%) i/s (418.40 ns/i) - 11.991M in 5.017466s
respond_to? 18.745M (± 1.8%) i/s (53.35 ns/i) - 93.843M in 5.007990s
Comparison:
respond_to?: 18744998.8 i/s
begin...rescue: 2390076.1 i/s - 7.84x slower
define_method vs module_eval for Defining Methods code
$ ruby -v code/general/define_method-vs-module-eval.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
module_eval with string
630.000 i/100ms
define_method 685.000 i/100ms
Calculating -------------------------------------
module_eval with string
5.633k (±18.1%) i/s (177.53 μs/i) - 27.090k in 5.009884s
define_method 7.239k (±17.6%) i/s (138.14 μs/i) - 34.250k in 5.040522s
Comparison:
define_method: 7238.9 i/s
module_eval with string: 5632.8 i/s - same-ish: difference falls within error
String#constantize vs a comparison for inflection code
ActiveSupport's String#constantize "resolves the constant reference expression in its receiver".
$ ruby -v code/general/constantize-vs-comparison.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
using an if statement
1.507M i/100ms
String#constantize 1.048M i/100ms
Calculating -------------------------------------
using an if statement
15.518M (± 1.2%) i/s (64.44 ns/i) - 78.355M in 5.050041s
String#constantize 10.556M (± 1.0%) i/s (94.73 ns/i) - 53.448M in 5.063570s
Comparison:
using an if statement: 15517955.2 i/s
String#constantize: 10556362.4 i/s - 1.47x slower
raise vs E2MM#Raise for raising (and defining) exceptions code
Ruby's Exception2MessageMapper module allows one to define and raise exceptions with predefined messages.
$ ruby -v code/general/raise-vs-e2mmap.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Ruby exception: E2MM#Raise
8.751k i/100ms
Ruby exception: Kernel#raise
254.268k i/100ms
Calculating -------------------------------------
Ruby exception: E2MM#Raise
88.269k (± 0.5%) i/s (11.33 μs/i) - 446.301k in 5.056287s
Ruby exception: Kernel#raise
2.571M (± 1.0%) i/s (389.01 ns/i) - 12.968M in 5.044976s
Comparison:
Ruby exception: Kernel#raise: 2570660.6 i/s
Ruby exception: E2MM#Raise: 88268.9 i/s - 29.12x slower
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Custom exception: E2MM#Raise
8.837k i/100ms
Custom exception: Kernel#raise
263.896k i/100ms
Calculating -------------------------------------
Custom exception: E2MM#Raise
88.322k (± 0.6%) i/s (11.32 μs/i) - 441.850k in 5.002885s
Custom exception: Kernel#raise
2.599M (± 2.1%) i/s (384.78 ns/i) - 13.195M in 5.079300s
Comparison:
Custom exception: Kernel#raise: 2598894.6 i/s
Custom exception: E2MM#Raise: 88322.1 i/s - 29.43x slower
loop vs while true code
$ ruby -v code/general/loop-vs-while-true.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
While Loop 1.000 i/100ms
Kernel loop 1.000 i/100ms
Calculating -------------------------------------
While Loop 1.331 (± 0.0%) i/s (751.41 ms/i) - 7.000 in 5.260296s
Kernel loop 0.528 (± 0.0%) i/s (1.89 s/i) - 3.000 in 5.709880s
Comparison:
While Loop: 1.3 i/s
Kernel loop: 0.5 i/s - 2.52x slower
ancestors.include? vs <= code
$ ruby -v code/general/inheritance-check.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
less than or equal 1.150M i/100ms
ancestors.include? 179.611k i/100ms
Calculating -------------------------------------
less than or equal 11.426M (± 1.4%) i/s (87.52 ns/i) - 57.493M in 5.032894s
ancestors.include? 1.880M (± 1.0%) i/s (531.84 ns/i) - 9.519M in 5.063302s
Comparison:
less than or equal: 11425608.6 i/s
ancestors.include?: 1880262.8 i/s - 6.08x slower
call vs send vs method_missing code
$ ruby -v code/method/call-vs-send-vs-method_missing.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
call 1.670M i/100ms
send 1.182M i/100ms
method_missing 837.965k i/100ms
Calculating -------------------------------------
call 16.799M (± 0.3%) i/s (59.53 ns/i) - 85.150M in 5.068737s
send 11.845M (± 0.3%) i/s (84.42 ns/i) - 60.281M in 5.089071s
method_missing 8.392M (± 0.3%) i/s (119.17 ns/i) - 42.736M in 5.092719s
Comparison:
call: 16799285.6 i/s
send: 11845293.0 i/s - 1.42x slower
method_missing: 8391692.6 i/s - 2.00x slower
Normal way to apply method vs &method(...) code
$ ruby -v code/general/block-apply-method.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
normal 698.536k i/100ms
&method 322.901k i/100ms
Calculating -------------------------------------
normal 6.978M (± 0.9%) i/s (143.31 ns/i) - 34.927M in 5.005888s
&method 3.214M (± 0.5%) i/s (311.12 ns/i) - 16.145M in 5.023139s
Comparison:
normal: 6977718.6 i/s
&method: 3214214.6 i/s - 2.17x slower
Function with single Array argument vs splat arguments code
$ ruby -v code/general/array-argument-vs-splat-arguments.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Function with single Array argument
1.910M i/100ms
Function with splat arguments
18.841k i/100ms
Calculating -------------------------------------
Function with single Array argument
18.818M (± 1.2%) i/s (53.14 ns/i) - 95.479M in 5.074468s
Function with splat arguments
217.060k (±11.2%) i/s (4.61 μs/i) - 1.093M in 5.099583s
Comparison:
Function with single Array argument: 18818207.9 i/s
Function with splat arguments: 217059.9 i/s - 86.70x slower
Hash vs OpenStruct on access assuming you already have a Hash or an OpenStruct code
$ ruby -v code/general/hash-vs-openstruct-on-access.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash 2.028M i/100ms
OpenStruct 1.478M i/100ms
Calculating -------------------------------------
Hash 20.290M (± 0.5%) i/s (49.29 ns/i) - 103.430M in 5.097815s
OpenStruct 14.807M (± 0.6%) i/s (67.54 ns/i) - 75.387M in 5.091599s
Comparison:
Hash: 20289714.4 i/s
OpenStruct: 14806564.8 i/s - 1.37x slower
Hash vs OpenStruct (creation) code
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash 2.652M i/100ms
OpenStruct 31.241k i/100ms
Calculating -------------------------------------
Hash 26.301M (± 1.0%) i/s (38.02 ns/i) - 132.614M in 5.042769s
OpenStruct 313.539k (± 1.8%) i/s (3.19 μs/i) - 1.593M in 5.083245s
Comparison:
Hash: 26300561.3 i/s
OpenStruct: 313538.7 i/s - 83.88x slower
Kernel#format vs Float#round().to_s code
$ ruby -v code/general/format-vs-round-and-to-s.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Float#round 465.786k i/100ms
Kernel#format 618.743k i/100ms
String#% 562.895k i/100ms
Calculating -------------------------------------
Float#round 4.780M (± 0.6%) i/s (209.21 ns/i) - 24.221M in 5.067359s
Kernel#format 6.200M (± 1.4%) i/s (161.29 ns/i) - 31.556M in 5.090736s
String#% 5.642M (± 1.1%) i/s (177.25 ns/i) - 28.708M in 5.088913s
Comparison:
Kernel#format: 6199975.0 i/s
String#%: 5641863.3 i/s - 1.10x slower
Float#round: 4779984.9 i/s - 1.30x slower
Array#bsearch vs Array#find code
WARNING: bsearch ONLY works on sorted array. More details please see #29.
$ ruby -v code/array/bsearch-vs-find.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
find 1.000 i/100ms
bsearch 189.387k i/100ms
Calculating -------------------------------------
find 0.703 (± 0.0%) i/s (1.42 s/i) - 4.000 in 5.692110s
bsearch 1.893M (± 1.6%) i/s (528.21 ns/i) - 9.469M in 5.003152s
Comparison:
bsearch: 1893194.5 i/s
find: 0.7 i/s - 2691605.95x slower
Array#length vs Array#size vs Array#count code
Use #length when you only want to know how many elements in the array, #count could also achieve this. However #count should be use for counting specific elements in array. Note #size is an alias of #length.
$ ruby -v code/array/length-vs-size-vs-count.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#length 5.024M i/100ms
Array#size 4.994M i/100ms
Array#count 3.781M i/100ms
Calculating -------------------------------------
Array#length 50.668M (± 0.8%) i/s (19.74 ns/i) - 256.237M in 5.057495s
Array#size 50.580M (± 0.7%) i/s (19.77 ns/i) - 254.704M in 5.035880s
Array#count 38.202M (± 0.8%) i/s (26.18 ns/i) - 192.843M in 5.048306s
Comparison:
Array#length: 50667869.8 i/s
Array#size: 50580088.8 i/s - same-ish: difference falls within error
Array#count: 38201768.5 i/s - 1.33x slower
Array#shuffle.first vs Array#sample code
Array#shuffleallocates an extra array.
Array#sampleindexes into the array without allocating an extra array.
This is the reason why Array#sample exists.
—— @sferik rails/rails#17245
$ ruby -v code/array/shuffle-first-vs-sample.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#shuffle.first 88.991k i/100ms
Array#sample 2.503M i/100ms
Calculating -------------------------------------
Array#shuffle.first 891.780k (± 0.5%) i/s (1.12 μs/i) - 4.539M in 5.089459s
Array#sample 25.133M (± 0.6%) i/s (39.79 ns/i) - 127.660M in 5.079516s
Comparison:
Array#sample: 25133099.8 i/s
Array#shuffle.first: 891779.8 i/s - 28.18x slower
Array#[](0) vs Array#first code
$ ruby -v code/array/array-first-vs-index.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#[0] 3.922M i/100ms
Array#first 3.697M i/100ms
Calculating -------------------------------------
Array#[0] 39.347M (± 0.7%) i/s (25.41 ns/i) - 200.006M in 5.083307s
Array#first 37.289M (± 1.1%) i/s (26.82 ns/i) - 188.546M in 5.056917s
Comparison:
Array#[0]: 39347487.6 i/s
Array#first: 37288935.3 i/s - 1.06x slower
Array#[](-1) vs Array#last code
$ ruby -v code/array/array-last-vs-index.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#[-1] 3.906M i/100ms
Array#last 3.727M i/100ms
Calculating -------------------------------------
Array#[-1] 39.303M (± 0.7%) i/s (25.44 ns/i) - 199.228M in 5.069210s
Array#last 37.280M (± 1.1%) i/s (26.82 ns/i) - 190.075M in 5.099254s
Comparison:
Array#[-1]: 39303493.5 i/s
Array#last: 37279563.5 i/s - 1.05x slower
Array#insert vs Array#unshift code
$ ruby -v code/array/insert-vs-unshift.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#unshift 37.000 i/100ms
Array#insert 1.000 i/100ms
Calculating -------------------------------------
Array#unshift 379.798 (± 1.1%) i/s (2.63 ms/i) - 1.924k in 5.066300s
Array#insert 1.717 (± 0.0%) i/s (582.49 ms/i) - 9.000 in 5.242429s
Comparison:
Array#unshift: 379.8 i/s
Array#insert: 1.7 i/s - 221.23x slower
Array#concat vs Array#+ code
Array#+ returns a new array built by concatenating the two arrays together to
produce a third array. Array#concat appends the elements of the other array to self.
This means that the + operator will create a new array each time it is called
(which is expensive), while concat only appends the new element.
$ ruby -v code/array/array-concat-vs-+.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#concat 126.000 i/100ms
Array#+ 1.000 i/100ms
Calculating -------------------------------------
Array#concat 1.259k (± 1.1%) i/s (793.98 μs/i) - 6.300k in 5.002769s
Array#+ 5.170 (± 0.0%) i/s (193.42 ms/i) - 26.000 in 5.033770s
Comparison:
Array#concat: 1259.5 i/s
Array#+: 5.2 i/s - 243.60x slower
Array#new vs Fixnum#times + map code
Typical slowdown is 40-60% depending on the size of the array. See the corresponding pull request for performance characteristics.
$ ruby -v code/array/array-new-vs-fixnum-times-map.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#new 424.914k i/100ms
Fixnum#times + map 187.997k i/100ms
Calculating -------------------------------------
Array#new 4.251M (± 0.4%) i/s (235.25 ns/i) - 21.671M in 5.098122s
Fixnum#times + map 1.886M (± 0.4%) i/s (530.31 ns/i) - 9.588M in 5.084632s
Comparison:
Array#new: 4250785.8 i/s
Fixnum#times + map: 1885679.9 i/s - 2.25x slower
Array#sort.reverse vs Array#sort_by + block code
$ ruby -v code/array/sort-reverse-vs-sort_by-with-block.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#sort.reverse 55.279k i/100ms
Array#sort_by &:-@ 22.773k i/100ms
Calculating -------------------------------------
Array#sort.reverse 560.570k (± 0.9%) i/s (1.78 μs/i) - 2.819M in 5.029648s
Array#sort_by &:-@ 229.324k (± 0.6%) i/s (4.36 μs/i) - 1.161M in 5.064717s
Comparison:
Array#sort.reverse: 560570.1 i/s
Array#sort_by &:-@: 229323.6 i/s - 2.44x slower
Enumerable#each + push vs Enumerable#map code
$ ruby -v code/enumerable/each-push-vs-map.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#each + push 31.310k i/100ms
Array#map 51.371k i/100ms
Calculating -------------------------------------
Array#each + push 320.334k (± 0.6%) i/s (3.12 μs/i) - 1.628M in 5.082743s
Array#map 511.854k (± 1.3%) i/s (1.95 μs/i) - 2.569M in 5.019061s
Comparison:
Array#map: 511854.5 i/s
Array#each + push: 320334.1 i/s - 1.60x slower
Enumerable#each vs for loop code
$ ruby -v code/enumerable/each-vs-for-loop.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
For loop 54.498k i/100ms
#each 63.984k i/100ms
Calculating -------------------------------------
For loop 525.548k (± 8.8%) i/s (1.90 μs/i) - 2.670M in 5.128917s
#each 641.789k (± 0.3%) i/s (1.56 μs/i) - 3.263M in 5.084548s
Comparison:
#each: 641788.6 i/s
For loop: 525547.8 i/s - 1.22x slower
Enumerable#each_with_index vs while loop code
$ ruby -v code/enumerable/each_with_index-vs-while-loop.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
While Loop 49.050k i/100ms
each_with_index 36.684k i/100ms
Calculating -------------------------------------
While Loop 501.430k (± 2.9%) i/s (1.99 μs/i) - 2.551M in 5.091049s
each_with_index 369.364k (± 0.3%) i/s (2.71 μs/i) - 1.871M in 5.065187s
Comparison:
While Loop: 501430.2 i/s
each_with_index: 369364.5 i/s - 1.36x slower
Enumerable#map...Array#flatten vs Enumerable#flat_map code
-- @sferik rails/rails@3413b88, Replace map.flatten with flat_map, Replace map.flatten(1) with flat_map
$ ruby -v code/enumerable/map-flatten-vs-flat_map.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#map.flatten(1) 19.072k i/100ms
Array#map.flatten 8.810k i/100ms
Array#flat_map 21.362k i/100ms
Calculating -------------------------------------
Array#map.flatten(1) 196.444k (± 1.2%) i/s (5.09 μs/i) - 991.744k in 5.049225s
Array#map.flatten 89.917k (± 1.2%) i/s (11.12 μs/i) - 458.120k in 5.095661s
Array#flat_map 215.149k (± 0.6%) i/s (4.65 μs/i) - 1.089M in 5.063916s
Comparison:
Array#flat_map: 215149.2 i/s
Array#map.flatten(1): 196443.8 i/s - 1.10x slower
Array#map.flatten: 89916.9 i/s - 2.39x slower
Enumerable#reverse.each vs Enumerable#reverse_each code
Enumerable#reverseallocates an extra array.
Enumerable#reverse_eachyields each value without allocating an extra array.
This is the reason whyEnumerable#reverse_eachexists.
-- @sferik rails/rails#17244
$ ruby -v code/enumerable/reverse-each-vs-reverse_each.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Array#reverse.each 58.610k i/100ms
Array#reverse_each 63.669k i/100ms
Calculating -------------------------------------
Array#reverse.each 586.309k (± 0.7%) i/s (1.71 μs/i) - 2.989M in 5.098469s
Array#reverse_each 635.904k (± 2.3%) i/s (1.57 μs/i) - 3.183M in 5.009371s
Comparison:
Array#reverse_each: 635904.1 i/s
Array#reverse.each: 586309.0 i/s - 1.08x slower
Enumerable#sort_by.first vs Enumerable#min_by code
Enumerable#sort_by performs a sort of the enumerable and allocates a
new array the size of the enumerable. Enumerable#min_by doesn't
perform a sort or allocate an array the size of the enumerable.
Similar comparisons hold for Enumerable#sort_by.last vs
Enumerable#max_by, Enumerable#sort.first vs Enumerable#min, and
Enumerable#sort.last vs Enumerable#max.
$ ruby -v code/enumerable/sort_by-first-vs-min_by.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Enumerable#min_by 40.784k i/100ms
Enumerable#sort_by...first
32.912k i/100ms
Calculating -------------------------------------
Enumerable#min_by 408.114k (± 0.3%) i/s (2.45 μs/i) - 2.080M in 5.096613s
Enumerable#sort_by...first
334.401k (± 0.7%) i/s (2.99 μs/i) - 1.679M in 5.019698s
Comparison:
Enumerable#min_by: 408113.9 i/s
Enumerable#sort_by...first: 334400.9 i/s - 1.22x slower
Enumerable#detect vs Enumerable#select.first code
$ ruby -v code/enumerable/select-first-vs-detect.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Enumerable#select.first
37.402k i/100ms
Enumerable#detect 247.638k i/100ms
Calculating -------------------------------------
Enumerable#select.first
373.115k (± 0.6%) i/s (2.68 μs/i) - 7.480M in 20.049160s
Enumerable#detect 2.462M (± 0.8%) i/s (406.12 ns/i) - 49.280M in 20.014730s
Comparison:
Enumerable#detect: 2462330.2 i/s
Enumerable#select.first: 373115.1 i/s - 6.60x slower
Enumerable#select.last vs Enumerable#reverse.detect code
$ ruby -v code/enumerable/select-last-vs-reverse-detect.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Enumerable#reverse.detect
23.133k i/100ms
Enumerable#select.last
562.000 i/100ms
Calculating -------------------------------------
Enumerable#reverse.detect
241.463k (± 5.5%) i/s (4.14 μs/i) - 1.226M in 5.092374s
Enumerable#select.last
5.640k (± 0.9%) i/s (177.30 μs/i) - 28.662k in 5.082075s
Comparison:
Enumerable#reverse.detect: 241463.1 i/s
Enumerable#select.last: 5640.3 i/s - 42.81x slower
Enumerable#sort vs Enumerable#sort_by code
$ ruby -v code/enumerable/sort-vs-sort_by.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Enumerable#sort_by (Symbol#to_proc)
16.077k i/100ms
Enumerable#sort_by 15.265k i/100ms
Enumerable#sort 4.580k i/100ms
Calculating -------------------------------------
Enumerable#sort_by (Symbol#to_proc)
162.709k (± 0.9%) i/s (6.15 μs/i) - 819.927k in 5.039602s
Enumerable#sort_by 152.611k (± 0.7%) i/s (6.55 μs/i) - 763.250k in 5.001512s
Enumerable#sort 45.814k (± 1.4%) i/s (21.83 μs/i) - 233.580k in 5.099352s
Comparison:
Enumerable#sort_by (Symbol#to_proc): 162709.4 i/s
Enumerable#sort_by: 152611.3 i/s - 1.07x slower
Enumerable#sort: 45814.3 i/s - 3.55x slower
Enumerable#inject Symbol vs Enumerable#inject Proc code
Of note, to_proc for 1.8.7 is considerable slower than the block format
$ ruby -v code/enumerable/inject-symbol-vs-block.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
inject symbol 183.846k i/100ms
inject to_proc 4.100k i/100ms
inject block 3.882k i/100ms
Calculating -------------------------------------
inject symbol 1.841M (± 0.4%) i/s (543.31 ns/i) - 9.376M in 5.094226s
inject to_proc 41.165k (± 0.3%) i/s (24.29 μs/i) - 209.100k in 5.079636s
inject block 39.550k (± 0.6%) i/s (25.28 μs/i) - 197.982k in 5.006073s
Comparison:
inject symbol: 1840578.1 i/s
inject to_proc: 41164.7 i/s - 44.71x slower
inject block: 39550.0 i/s - 46.54x slower
Date.iso8601 vs Date.parse code
When expecting well-formatted data from e.g. an API, iso8601 is faster and will raise an ArgumentError on malformed input.
$ ruby -v code/date/iso8601-vs-parse.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Date.iso8601 195.684k i/100ms
Date.parse 62.633k i/100ms
Calculating -------------------------------------
Date.iso8601 2.008M (± 0.8%) i/s (497.99 ns/i) - 10.176M in 5.067673s
Date.parse 627.349k (± 0.7%) i/s (1.59 μs/i) - 3.194M in 5.092000s
Comparison:
Date.iso8601: 2008075.4 i/s
Date.parse: 627349.2 i/s - 3.20x slower
Hash#[] vs Hash#fetch code
If you use Ruby 2.2, Symbol could be more performant than String as Hash keys.
Read more regarding this: Symbol GC in Ruby 2.2 and Unraveling String Key Performance in Ruby 2.2.
$ ruby -v code/hash/bracket-vs-fetch.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#[], symbol 3.471M i/100ms
Hash#fetch, symbol 2.869M i/100ms
Hash#[], string 1.969M i/100ms
Hash#fetch, string 1.746M i/100ms
Calculating -------------------------------------
Hash#[], symbol 34.983M (± 0.8%) i/s (28.59 ns/i) - 177.026M in 5.060743s
Hash#fetch, symbol 28.927M (± 1.1%) i/s (34.57 ns/i) - 146.331M in 5.059120s
Hash#[], string 19.711M (± 1.7%) i/s (50.73 ns/i) - 100.431M in 5.096713s
Hash#fetch, string 17.825M (± 1.4%) i/s (56.10 ns/i) - 90.775M in 5.093555s
Comparison:
Hash#[], symbol: 34982573.5 i/s
Hash#fetch, symbol: 28927450.2 i/s - 1.21x slower
Hash#[], string: 19710678.7 i/s - 1.77x slower
Hash#fetch, string: 17825222.3 i/s - 1.96x slower
Hash#dig vs Hash#[] vs Hash#fetch code
Ruby 2.3 introduced Hash#dig which is a readable
and performant option for retrieval from a nested hash, returning nil if an extraction step fails.
See #102 (comment) for more info.
$ ruby -v code/hash/dig-vs-[]-vs-fetch.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#dig 1.939M i/100ms
Hash#[] 1.903M i/100ms
Hash#[] || 1.787M i/100ms
Hash#[] && 725.944k i/100ms
Hash#fetch 1.246M i/100ms
Hash#fetch fallback 803.269k i/100ms
Calculating -------------------------------------
Hash#dig 19.434M (± 0.8%) i/s (51.46 ns/i) - 98.877M in 5.088095s
Hash#[] 19.005M (± 2.2%) i/s (52.62 ns/i) - 95.141M in 5.008511s
Hash#[] || 17.879M (± 0.7%) i/s (55.93 ns/i) - 91.138M in 5.097648s
Hash#[] && 7.303M (± 0.6%) i/s (136.93 ns/i) - 37.023M in 5.069773s
Hash#fetch 12.634M (± 2.0%) i/s (79.15 ns/i) - 63.570M in 5.033708s
Hash#fetch fallback 8.117M (± 1.0%) i/s (123.20 ns/i) - 40.967M in 5.047539s
Comparison:
Hash#dig: 19434078.6 i/s
Hash#[]: 19004999.7 i/s - same-ish: difference falls within error
Hash#[] ||: 17879337.5 i/s - 1.09x slower
Hash#fetch: 12633982.2 i/s - 1.54x slower
Hash#fetch fallback: 8116930.3 i/s - 2.39x slower
Hash#[] &&: 7302991.9 i/s - 2.66x slower
Hash[] vs Hash#dup code
Source: http://tenderlovemaking.com/2015/02/11/weird-stuff-with-hashes.html
Does this mean that you should switch to Hash[]? Only if your benchmarks can prove that it’s a bottleneck. Please please please don’t change all of your code because this shows it’s faster. Make sure to measure your app performance first.
$ ruby -v code/hash/bracket-vs-dup.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash[] 684.060k i/100ms
Hash#dup 612.214k i/100ms
Calculating -------------------------------------
Hash[] 7.555M (± 2.8%) i/s (132.37 ns/i) - 38.307M in 5.074888s
Hash#dup 5.851M (± 2.1%) i/s (170.90 ns/i) - 29.386M in 5.024310s
Comparison:
Hash[]: 7554758.4 i/s
Hash#dup: 5851408.7 i/s - 1.29x slower
Hash#fetch with argument vs Hash#fetch + block code
Note that the speedup in the block version comes from avoiding repeated
construction of the argument. If the argument is a constant, number symbol or
something of that sort the argument version is actually slightly faster
See also #39 (comment)
$ ruby -v code/hash/fetch-vs-fetch-with-block.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#fetch + const 3.136M i/100ms
Hash#fetch + block 3.313M i/100ms
Hash#fetch + arg 2.266M i/100ms
Calculating -------------------------------------
Hash#fetch + const 32.198M (± 1.0%) i/s (31.06 ns/i) - 163.090M in 5.065722s
Hash#fetch + block 33.319M (± 1.1%) i/s (30.01 ns/i) - 168.953M in 5.071324s
Hash#fetch + arg 22.838M (± 1.3%) i/s (43.79 ns/i) - 115.561M in 5.060964s
Comparison:
Hash#fetch + block: 33319415.6 i/s
Hash#fetch + const: 32198246.2 i/s - 1.03x slower
Hash#fetch + arg: 22837560.4 i/s - 1.46x slower
Hash#each_key instead of Hash#keys.each code
Hash#keys.eachallocates an array of keys;
Hash#each_keyiterates through the keys without allocating a new array.
This is the reason whyHash#each_keyexists.
—— @sferik rails/rails#17099
$ ruby -v code/hash/keys-each-vs-each_key.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#keys.each 494.495k i/100ms
Hash#each_key 510.269k i/100ms
Calculating -------------------------------------
Hash#keys.each 5.026M (± 0.5%) i/s (198.95 ns/i) - 25.219M in 5.017574s
Hash#each_key 5.118M (± 1.2%) i/s (195.39 ns/i) - 26.024M in 5.085610s
Comparison:
Hash#each_key: 5117890.6 i/s
Hash#keys.each: 5026302.4 i/s - 1.02x slower
Hash#key? instead of Hash#keys.include? code
Hash#keys.include?allocates an array of keys and performs an O(n) search;
Hash#key?performs an O(1) hash lookup without allocating a new array.
$ ruby -v code/hash/keys-include-vs-key.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#keys.include? 2.217k i/100ms
Hash#key? 2.683M i/100ms
Calculating -------------------------------------
Hash#keys.include? 26.005k (± 5.6%) i/s (38.45 μs/i) - 130.803k in 5.047937s
Hash#key? 26.686M (± 1.3%) i/s (37.47 ns/i) - 134.156M in 5.028106s
Comparison:
Hash#key?: 26685586.3 i/s
Hash#keys.include?: 26005.2 i/s - 1026.16x slower
Hash#value? instead of Hash#values.include? code
Hash#values.include?allocates an array of values and performs an O(n) search;
Hash#value?performs an O(n) search without allocating a new array.
$ ruby -v code/hash/values-include-vs-value.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#values.include? 5.620k i/100ms
Hash#value? 7.474k i/100ms
Calculating -------------------------------------
Hash#values.include? 62.053k (± 2.8%) i/s (16.12 μs/i) - 314.720k in 5.076009s
Hash#value? 71.151k (± 4.4%) i/s (14.05 μs/i) - 358.752k in 5.051774s
Comparison:
Hash#value?: 71150.8 i/s
Hash#values.include?: 62052.8 i/s - 1.15x slower
Hash#merge! vs Hash#[]= code
$ ruby -v code/hash/merge-bang-vs-[]=.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#merge! 8.974k i/100ms
Hash#[]= 20.052k i/100ms
Calculating -------------------------------------
Hash#merge! 91.391k (± 0.5%) i/s (10.94 μs/i) - 457.674k in 5.008010s
Hash#[]= 202.686k (± 0.9%) i/s (4.93 μs/i) - 1.023M in 5.045877s
Comparison:
Hash#[]=: 202686.0 i/s
Hash#merge!: 91390.8 i/s - 2.22x slower
Hash#update vs Hash#[]= code
$ ruby -v code/hash/update-vs-[]=.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#[]= 19.739k i/100ms
Hash#update 9.126k i/100ms
Calculating -------------------------------------
Hash#[]= 202.714k (± 1.1%) i/s (4.93 μs/i) - 1.026M in 5.064063s
Hash#update 91.615k (± 0.7%) i/s (10.92 μs/i) - 465.426k in 5.080450s
Comparison:
Hash#[]=: 202713.9 i/s
Hash#update: 91615.2 i/s - 2.21x slower
Hash#merge vs Hash#**other code
$ ruby -v code/hash/merge-vs-double-splat-operator.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#**other 979.615k i/100ms
Hash#merge 798.516k i/100ms
Calculating -------------------------------------
Hash#**other 9.846M (± 0.3%) i/s (101.57 ns/i) - 49.960M in 5.074265s
Hash#merge 7.968M (± 0.3%) i/s (125.50 ns/i) - 39.926M in 5.010730s
Comparison:
Hash#**other: 9845909.6 i/s
Hash#merge: 7968122.7 i/s - 1.24x slower
Hash#merge vs Hash#merge! code
$ ruby -v code/hash/merge-vs-merge-bang.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#merge 3.800k i/100ms
Hash#merge! 9.310k i/100ms
Calculating -------------------------------------
Hash#merge 38.042k (± 1.9%) i/s (26.29 μs/i) - 193.800k in 5.096234s
Hash#merge! 94.284k (± 0.4%) i/s (10.61 μs/i) - 474.810k in 5.036016s
Comparison:
Hash#merge!: 94284.1 i/s
Hash#merge: 38041.7 i/s - 2.48x slower
{}#merge!(Hash) vs Hash#merge({}) vs Hash#dup#merge!({}) code
When we don't want to modify the original hash, and we want duplicates to be created
See #42 for more details.
$ ruby -v code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
{}#merge!(Hash) do end
11.079k i/100ms
Hash#merge({}) 9.290k i/100ms
Hash#dup#merge!({}) 6.957k i/100ms
Calculating -------------------------------------
{}#merge!(Hash) do end
111.999k (± 0.3%) i/s (8.93 μs/i) - 565.029k in 5.045023s
Hash#merge({}) 93.149k (± 0.4%) i/s (10.74 μs/i) - 473.790k in 5.086441s
Hash#dup#merge!({}) 69.802k (± 0.4%) i/s (14.33 μs/i) - 354.807k in 5.083113s
Comparison:
{}#merge!(Hash) do end: 111998.6 i/s
Hash#merge({}): 93149.0 i/s - 1.20x slower
Hash#dup#merge!({}): 69802.3 i/s - 1.60x slower
Hash#sort_by vs Hash#sort code
To sort hash by key.
$ ruby -v code/hash/hash-key-sort_by-vs-sort.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
sort_by + to_h 72.056k i/100ms
sort + to_h 26.595k i/100ms
Calculating -------------------------------------
sort_by + to_h 739.508k (± 0.7%) i/s (1.35 μs/i) - 3.747M in 5.067024s
sort + to_h 269.700k (± 1.9%) i/s (3.71 μs/i) - 1.356M in 5.030958s
Comparison:
sort_by + to_h: 739507.8 i/s
sort + to_h: 269700.3 i/s - 2.74x slower
Native Hash#slice vs other slice implementations before native code
Since ruby 2.5, Hash comes with a slice method to select hash members by keys.
$ ruby -v code/hash/slice-native-vs-before-native.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Hash#native-slice 1.024M i/100ms
Array#each 467.292k i/100ms
Array#each_w/_object 388.528k i/100ms
Hash#select-include 131.358k i/100ms
Calculating -------------------------------------
Hash#native-slice 10.550M (± 1.4%) i/s (94.78 ns/i) - 53.251M in 5.048257s
Array#each 4.688M (± 0.5%) i/s (213.30 ns/i) - 23.832M in 5.083384s
Array#each_w/_object 3.890M (± 0.4%) i/s (257.07 ns/i) - 19.815M in 5.093848s
Hash#select-include 1.343M (± 1.7%) i/s (744.63 ns/i) - 6.831M in 5.087848s
Comparison:
Hash#native-slice : 10550464.1 i/s
Array#each : 4688305.3 i/s - 2.25x slower
Array#each_w/_object: 3890033.7 i/s - 2.71x slower
Hash#select-include : 1342942.2 i/s - 7.86x slower
Block vs Symbol#to_proc code
Symbol#to_procis considerably more concise than using block syntax.
...In some cases, it reduces the number of lines of code.
—— @sferik rails/rails#16833
$ ruby -v code/proc-and-block/block-vs-to_proc.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Block 20.147k i/100ms
Symbol#to_proc 22.231k i/100ms
Calculating -------------------------------------
Block 202.937k (± 0.4%) i/s (4.93 μs/i) - 1.027M in 5.063220s
Symbol#to_proc 222.450k (± 0.9%) i/s (4.50 μs/i) - 1.134M in 5.097179s
Comparison:
Symbol#to_proc: 222449.5 i/s
Block: 202937.5 i/s - 1.10x slower
Proc#call and block arguments vs yield code
$ ruby -v code/proc-and-block/proc-call-vs-yield.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
block.call 2.261M i/100ms
block + yield 2.314M i/100ms
unused block 3.025M i/100ms
yield 2.971M i/100ms
Calculating -------------------------------------
block.call 22.057M (± 6.0%) i/s (45.34 ns/i) - 110.796M in 5.043129s
block + yield 23.280M (± 0.6%) i/s (42.96 ns/i) - 117.997M in 5.068779s
unused block 30.609M (± 1.3%) i/s (32.67 ns/i) - 154.268M in 5.040991s
yield 29.921M (± 0.6%) i/s (33.42 ns/i) - 151.512M in 5.063842s
Comparison:
unused block: 30608512.5 i/s
yield: 29921356.8 i/s - 1.02x slower
block + yield: 23279981.0 i/s - 1.31x slower
block.call: 22056758.6 i/s - 1.39x slower
String#dup vs String#+ code
Note that String.new is not the same as the options compared, since it is
always ASCII-8BIT encoded instead of the script encoding (usually UTF-8).
$ ruby -v code/string/dup-vs-unary-plus.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#+@ 2.439M i/100ms
String#dup 2.481M i/100ms
Calculating -------------------------------------
String#+@ 24.328M (± 0.5%) i/s (41.10 ns/i) - 121.962M in 5.013305s
String#dup 24.553M (± 1.0%) i/s (40.73 ns/i) - 124.040M in 5.052462s
Comparison:
String#dup: 24552887.2 i/s
String#+@: 24328187.6 i/s - same-ish: difference falls within error
String#casecmp vs String#casecmp? vs String#downcase + == code
String#casecmp? is available on Ruby 2.4 or later.
Note that String#casecmp only works on characters A-Z/a-z, not all of Unicode.
$ ruby -v code/string/casecmp-vs-downcase-==.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#casecmp? 1.053M i/100ms
String#downcase + == 1.467M i/100ms
String#casecmp 1.816M i/100ms
Calculating -------------------------------------
String#casecmp? 10.916M (± 1.4%) i/s (91.61 ns/i) - 54.769M in 5.018429s
String#downcase + == 14.673M (± 1.0%) i/s (68.15 ns/i) - 74.808M in 5.098814s
String#casecmp 18.210M (± 0.7%) i/s (54.91 ns/i) - 92.594M in 5.084879s
Comparison:
String#casecmp: 18210413.9 i/s
String#downcase + ==: 14673089.8 i/s - 1.24x slower
String#casecmp?: 10915954.0 i/s - 1.67x slower
String Concatenation code
$ ruby -v code/string/concatenation.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#+ 1.214M i/100ms
String#concat 1.407M i/100ms
String#append 1.561M i/100ms
"foo" "bar" 2.723M i/100ms
"#{'foo'}#{'bar'}" 2.914M i/100ms
Calculating -------------------------------------
String#+ 12.616M (± 0.9%) i/s (79.27 ns/i) - 63.138M in 5.005092s
String#concat 14.555M (± 1.1%) i/s (68.71 ns/i) - 73.188M in 5.029155s
String#append 15.844M (± 0.7%) i/s (63.12 ns/i) - 79.634M in 5.026390s
"foo" "bar" 27.318M (± 0.8%) i/s (36.61 ns/i) - 138.888M in 5.084517s
"#{'foo'}#{'bar'}" 29.063M (± 0.5%) i/s (34.41 ns/i) - 145.707M in 5.013531s
Comparison:
"#{'foo'}#{'bar'}": 29063458.4 i/s
"foo" "bar": 27317882.5 i/s - 1.06x slower
String#append: 15843896.8 i/s - 1.83x slower
String#concat: 14554534.2 i/s - 2.00x slower
String#+: 12615859.7 i/s - 2.30x slower
String#match vs String.match? vs String#start_with?/String#end_with? code (start) code (end)
The regular expression approaches become slower as the tested string becomes
longer. For short strings, String#match? performs similarly to
String#start_with?/String#end_with?.
⚠️
Sometimes you cant replace regexp withstart_with?,
for example:"a\nb" =~ /^b/ #=> 2but"a\nb" =~ /\Ab/ #=> nil.
⚠️
$ ruby -v code/string/start-string-checking-match-vs-start_with.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#=~ 704.323k i/100ms
String#match? 1.696M i/100ms
String#start_with? 2.002M i/100ms
Calculating -------------------------------------
String#=~ 7.155M (± 0.9%) i/s (139.76 ns/i) - 35.920M in 5.020673s
String#match? 17.088M (± 0.7%) i/s (58.52 ns/i) - 86.478M in 5.061156s
String#start_with? 20.186M (± 1.0%) i/s (49.54 ns/i) - 102.087M in 5.057969s
Comparison:
String#start_with?: 20185554.4 i/s
String#match?: 17087554.1 i/s - 1.18x slower
String#=~: 7155069.8 i/s - 2.82x slower
$ ruby -v code/string/end-string-checking-match-vs-end_with.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#=~ 459.289k i/100ms
String#match? 808.294k i/100ms
String#end_with? 1.358M i/100ms
Calculating -------------------------------------
String#=~ 4.900M (± 0.7%) i/s (204.10 ns/i) - 24.802M in 5.062282s
String#match? 7.999M (± 3.2%) i/s (125.02 ns/i) - 40.415M in 5.058576s
String#end_with? 13.797M (± 0.9%) i/s (72.48 ns/i) - 69.248M in 5.019420s
Comparison:
String#end_with?: 13797085.6 i/s
String#match?: 7998569.4 i/s - 1.72x slower
String#=~: 4899556.6 i/s - 2.82x slower
String#start_with? vs String#[].== code
$ ruby -v code/string/start_with-vs-substring-==.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#start_with? 405.198k i/100ms
String#[0, n] == 185.548k i/100ms
String#[RANGE] == 183.537k i/100ms
String#[0...n] == 111.844k i/100ms
Calculating -------------------------------------
String#start_with? 4.336M (± 1.7%) i/s (230.64 ns/i) - 21.881M in 5.048171s
String#[0, n] == 1.911M (± 1.8%) i/s (523.18 ns/i) - 9.648M in 5.049643s
String#[RANGE] == 1.835M (± 1.4%) i/s (544.95 ns/i) - 9.177M in 5.001963s
String#[0...n] == 1.100M (± 1.5%) i/s (909.34 ns/i) - 5.592M in 5.086419s
Comparison:
String#start_with?: 4335677.0 i/s
String#[0, n] ==: 1911392.8 i/s - 2.27x slower
String#[RANGE] ==: 1835034.3 i/s - 2.36x slower
String#[0...n] ==: 1099697.5 i/s - 3.94x slower
Regexp#=== vs Regexp#match vs Regexp#match? vs String#match vs String#=~ vs String#match? code
String#match? and Regexp#match? are available on Ruby 2.4 or later.
ActiveSupport provides
a forward compatible extension of Regexp for older Rubies without the speed
improvement.
⚠️
Sometimes you can't replacematchwithmatch?,
This is only useful for cases where you are checking
for a match and not using the resultant match object.
⚠️
Regexp#===is also faster thanString#matchbut you need to switch the order of arguments.
$ ruby -v code/string/===-vs-=~-vs-match.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Regexp#match? 2.240M i/100ms
String#match? 2.271M i/100ms
String#=~ 1.322M i/100ms
Regexp#=== 1.252M i/100ms
Regexp#match 1.187M i/100ms
String#match 1.024M i/100ms
Calculating -------------------------------------
Regexp#match? 22.395M (± 1.3%) i/s (44.65 ns/i) - 111.995M in 5.001594s
String#match? 22.544M (± 1.4%) i/s (44.36 ns/i) - 113.533M in 5.037001s
String#=~ 13.285M (± 2.6%) i/s (75.27 ns/i) - 67.438M in 5.079611s
Regexp#=== 12.472M (± 0.6%) i/s (80.18 ns/i) - 62.618M in 5.020860s
Regexp#match 11.865M (± 0.8%) i/s (84.28 ns/i) - 59.340M in 5.001611s
String#match 10.223M (± 0.7%) i/s (97.81 ns/i) - 51.194M in 5.007796s
Comparison:
String#match?: 22544340.1 i/s
Regexp#match?: 22395457.7 i/s - same-ish: difference falls within error
String#=~: 13285297.2 i/s - 1.70x slower
Regexp#===: 12471978.3 i/s - 1.81x slower
Regexp#match: 11864949.7 i/s - 1.90x slower
String#match: 10223419.6 i/s - 2.21x slower
See #59 and #62 for discussions.
String#gsub vs String#sub vs String#[]= code
$ ruby -v code/string/gsub-vs-sub.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#gsub 230.980k i/100ms
String#sub 465.575k i/100ms
String#dup["string"]=
838.284k i/100ms
Calculating -------------------------------------
String#gsub 2.338M (± 0.9%) i/s (427.64 ns/i) - 11.780M in 5.038072s
String#sub 4.691M (± 1.2%) i/s (213.16 ns/i) - 23.744M in 5.062169s
String#dup["string"]=
8.433M (± 1.0%) i/s (118.58 ns/i) - 42.752M in 5.070311s
Comparison:
String#dup["string"]=: 8432793.1 i/s
String#sub: 4691246.0 i/s - 1.80x slower
String#gsub: 2338396.7 i/s - 3.61x slower
String#gsub vs String#tr code
$ ruby -v code/string/gsub-vs-tr.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#gsub 297.314k i/100ms
String#tr 914.116k i/100ms
Calculating -------------------------------------
String#gsub 3.032M (± 0.8%) i/s (329.83 ns/i) - 15.163M in 5.001554s
String#tr 9.136M (± 0.8%) i/s (109.46 ns/i) - 45.706M in 5.003201s
Comparison:
String#tr: 9135960.5 i/s
String#gsub: 3031853.2 i/s - 3.01x slower
String#gsub vs String#tr vs String#delete code
$ ruby -v code/string/gsub-vs-tr-vs-delete.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#gsub 314.117k i/100ms
String#tr 989.154k i/100ms
String#delete 1.159M i/100ms
String#delete const 1.320M i/100ms
Calculating -------------------------------------
String#gsub 3.099M (± 1.1%) i/s (322.65 ns/i) - 15.706M in 5.068096s
String#tr 9.868M (± 1.3%) i/s (101.34 ns/i) - 49.458M in 5.012992s
String#delete 11.623M (± 0.6%) i/s (86.03 ns/i) - 59.109M in 5.085570s
String#delete const 13.192M (± 0.8%) i/s (75.80 ns/i) - 65.989M in 5.002525s
Comparison:
String#delete const: 13191917.5 i/s
String#delete: 11623217.0 i/s - 1.13x slower
String#tr: 9867528.0 i/s - 1.34x slower
String#gsub: 3099363.7 i/s - 4.26x slower
Mutable vs Immutable code
$ ruby -v code/string/mutable_vs_immutable_strings.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Without Freeze 2.671M i/100ms
With Freeze 4.464M i/100ms
Calculating -------------------------------------
Without Freeze 27.091M (± 1.6%) i/s (36.91 ns/i) - 136.216M in 5.029377s
With Freeze 44.763M (± 0.9%) i/s (22.34 ns/i) - 227.646M in 5.085943s
Comparison:
With Freeze: 44763124.8 i/s
Without Freeze: 27091288.1 i/s - 1.65x slower
String#sub! vs String#gsub! vs String#[]= code
Note that String#[] will throw an IndexError when given string or regexp not matched.
$ ruby -v code/string/sub!-vs-gsub!-vs-[]=.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#['string']= 867.608k i/100ms
String#sub!'string' 452.774k i/100ms
String#gsub!'string' 219.002k i/100ms
String#[/regexp/]= 484.258k i/100ms
String#sub!/regexp/ 439.965k i/100ms
String#gsub!/regexp/ 222.202k i/100ms
Calculating -------------------------------------
String#['string']= 8.875M (± 0.9%) i/s (112.67 ns/i) - 45.116M in 5.083636s
String#sub!'string' 4.648M (± 0.6%) i/s (215.15 ns/i) - 23.544M in 5.065834s
String#gsub!'string' 2.234M (± 0.6%) i/s (447.72 ns/i) - 11.169M in 5.000776s
String#[/regexp/]= 4.913M (± 0.6%) i/s (203.54 ns/i) - 24.697M in 5.026953s
String#sub!/regexp/ 4.474M (± 0.4%) i/s (223.50 ns/i) - 22.438M in 5.014974s
String#gsub!/regexp/ 2.221M (± 0.7%) i/s (450.25 ns/i) - 11.110M in 5.002620s
Comparison:
String#['string']=: 8875438.0 i/s
String#[/regexp/]=: 4913121.6 i/s - 1.81x slower
String#sub!'string': 4647843.8 i/s - 1.91x slower
String#sub!/regexp/: 4474302.7 i/s - 1.98x slower
String#gsub!'string': 2233558.9 i/s - 3.97x slower
String#gsub!/regexp/: 2220978.6 i/s - 4.00x slower
String#sub vs String#delete_prefix code
Ruby 2.5 introduced String#delete_prefix.
Note that this can only be used for removing characters from the start of a string.
$ ruby -v code/string/sub-vs-delete_prefix.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#delete_prefix 1.446M i/100ms
String#sub 473.108k i/100ms
Calculating -------------------------------------
String#delete_prefix 14.867M (± 1.3%) i/s (67.26 ns/i) - 75.200M in 5.059147s
String#sub 4.726M (± 0.7%) i/s (211.58 ns/i) - 23.655M in 5.005369s
Comparison:
String#delete_prefix: 14866723.8 i/s
String#sub: 4726255.3 i/s - 3.15x slower
String#sub vs String#chomp vs String#delete_suffix code
Ruby 2.5 introduced String#delete_suffix
as a counterpart to delete_prefix. The performance gain over chomp is
small and during some runs the difference falls within the error margin.
Note that this can only be used for removing characters from the end of a string.
$ ruby -v code/string/sub-vs-chomp-vs-delete_suffix.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#sub 472.278k i/100ms
String#chomp 1.354M i/100ms
String#delete_suffix 1.477M i/100ms
Calculating -------------------------------------
String#sub 4.809M (± 1.0%) i/s (207.93 ns/i) - 24.086M in 5.008842s
String#chomp 13.829M (± 1.2%) i/s (72.31 ns/i) - 70.425M in 5.093208s
String#delete_suffix 14.936M (± 0.9%) i/s (66.95 ns/i) - 75.317M in 5.042961s
Comparison:
String#delete_suffix: 14936380.8 i/s
String#chomp: 13829135.7 i/s - 1.08x slower
String#sub: 4809249.5 i/s - 3.11x slower
String#unpack1 vs String#unpack[0] code
Ruby 2.4.0 introduced unpack1 to skip creating the intermediate array object.
$ ruby -v code/string/unpack1-vs-unpack[0].rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#unpack1 1.372M i/100ms
String#unpack[0] 1.095M i/100ms
Calculating -------------------------------------
String#unpack1 14.219M (± 0.7%) i/s (70.33 ns/i) - 71.349M in 5.018051s
String#unpack[0] 11.071M (± 1.2%) i/s (90.33 ns/i) - 55.839M in 5.044380s
Comparison:
String#unpack1: 14219190.8 i/s
String#unpack[0]: 11071030.6 i/s - 1.28x slower
Remove extra spaces (or other contiguous characters) code
The code is tested against contiguous spaces but should work for other chars too.
$ ruby -v code/string/remove-extra-spaces-or-other-chars.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
String#gsub/regex+/ 15.087k i/100ms
String#squeeze 429.561k i/100ms
Calculating -------------------------------------
String#gsub/regex+/ 152.509k (± 0.6%) i/s (6.56 μs/i) - 769.437k in 5.045412s
String#squeeze 4.264M (± 1.7%) i/s (234.51 ns/i) - 21.478M in 5.038218s
Comparison:
String#squeeze: 4264261.4 i/s
String#gsub/regex+/: 152508.7 i/s - 27.96x slower
Time.iso8601 vs Time.parse code
When expecting well-formatted data from e.g. an API, iso8601 is faster and will raise an ArgumentError on malformed input.
$ ruby -v code/time/iso8601-vs-parse.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
Time.iso8601 105.846k i/100ms
Time.parse 19.826k i/100ms
Calculating -------------------------------------
Time.iso8601 1.076M (± 0.5%) i/s (929.16 ns/i) - 5.398M in 5.015850s
Time.parse 200.370k (± 0.8%) i/s (4.99 μs/i) - 1.011M in 5.046598s
Comparison:
Time.iso8601: 1076241.3 i/s
Time.parse: 200370.2 i/s - 5.37x slower
cover? vs include? code
cover? only check if it is within the start and end, include? needs to traverse the whole range.
$ ruby -v code/range/cover-vs-include.rb
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24]
Warming up --------------------------------------
range#cover? 811.176k i/100ms
range#include? 34.101k i/100ms
range#member? 35.591k i/100ms
plain compare 1.131M i/100ms
value.between? 1.520M i/100ms
Calculating -------------------------------------
range#cover? 8.324M (± 0.3%) i/s (120.14 ns/i) - 42.181M in 5.067623s
range#include? 352.846k (± 1.7%) i/s (2.83 μs/i) - 1.773M in 5.027064s
range#member? 349.526k (± 1.9%) i/s (2.86 μs/i) - 1.780M in 5.093144s
plain compare 11.296M (± 1.3%) i/s (88.53 ns/i) - 56.554M in 5.007498s
value.between? 15.214M (± 0.9%) i/s (65.73 ns/i) - 77.527M in 5.096183s
Comparison:
value.between?: 15213944.1 i/s
plain compare: 11295903.4 i/s - 1.35x slower
range#cover?: 8323723.8 i/s - 1.83x slower
range#include?: 352846.2 i/s - 43.12x slower
range#member?: 349526.0 i/s - 43.53x slower
Please! Edit this README.md then Submit a Awesome Pull Request!
Code example is wrong? 😢 Got better example? 😍 Excellent!
Please open an issue or Open a Pull Request to fix it.
Thank you in advance! 😉 🍺
Share this with your #Rubyfriends! <3
Brought to you by @JuanitoFatas
Feel free to talk with me on Twitter! <3
-
Go faster, off the Rails - Benchmarks for your whole Rails app
-
Talk by Davy Stevenson @ RubyConf 2014.
-
Provides Big O notation benchmarking for Ruby.
-
Talk by Prem Sichanugrist @ Ruby Kaigi 2014.
-
Make your Rubies go faster with this command line tool.
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
To the extent possible under law, @JuanitoFatas has waived all copyright and related or neighboring rights to "fast-ruby".
This work belongs to the community.
