From 68f71c55efb3255db5799194101ca9978f52ea12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Thu, 24 Sep 2026 11:06:21 -0600 Subject: [PATCH 1/4] Step 1: Name every benchmark's winner in its method names Each benchmark now says which report should win through its method names: `fastest`, `faster`, `fast`, then `slow`, `slower`, `slowest`. Every report is wrapped in one of these methods, so they all pay the same call cost, and the winner is listed first. Where a ranking could look odd, a one-line comment says why. - 11 files that ran code inline or used other names were converted. - start_with-vs-substring-==.rb reported code strings; each string now becomes a method, so its numbers are not comparable with older runs. - raise-vs-e2mmap.rb had two blocks that could not both claim `fast`; the custom exception block moved to raise-custom-vs-e2mmap.rb. - 21 files that already used `fast` listed a slower report first; only their report lines were reordered. - keys-include and values-include dropped `.shuffle`. It moved the looked-up item on every run, so the winner of values-include depended on where it landed. Both now look up the entry halfway through. Rankings follow the results from CI run 35927028164. --- README.md | 5 +- code/array/bsearch-vs-find.rb | 14 +++-- code/array/insert-vs-unshift.rb | 20 ++++---- code/array/length-vs-size-vs-count.rb | 19 +++++-- code/array/shuffle-first-vs-sample.rb | 2 +- code/enumerable/each-push-vs-map.rb | 2 +- code/enumerable/each-vs-for-loop.rb | 2 +- code/enumerable/map-flatten-vs-flat_map.rb | 2 +- .../reverse-each-vs-reverse_each.rb | 2 +- code/enumerable/select-first-vs-detect.rb | 2 +- .../attr-accessor-vs-getter-and-setter.rb | 2 +- code/general/begin-rescue-vs-respond-to.rb | 2 +- code/general/define_method-vs-module-eval.rb | 2 +- code/general/raise-custom-vs-e2mmap.rb | 45 ++++++++++++++++ code/general/raise-vs-e2mmap.rb | 46 +++-------------- code/hash/dig-vs-[]-vs-fetch.rb | 51 +++++++++++-------- code/hash/fetch-vs-fetch-with-block.rb | 22 ++++++-- code/hash/keys-each-vs-each_key.rb | 2 +- code/hash/keys-include-vs-key.rb | 12 ++--- code/hash/merge-bang-vs-[]=.rb | 2 +- code/hash/merge-vs-merge-bang.rb | 2 +- code/hash/values-include-vs-value.rb | 14 ++--- code/proc-and-block/block-vs-to_proc.rb | 2 +- code/proc-and-block/proc-call-vs-yield.rb | 2 +- code/range/cover-vs-include.rb | 33 +++++++++--- code/string/casecmp-vs-downcase-==.rb | 4 +- code/string/concatenation.rb | 40 ++++++++------- .../end-string-checking-match-vs-end_with.rb | 4 +- code/string/gsub-vs-sub.rb | 4 +- code/string/gsub-vs-tr-vs-delete.rb | 25 ++++----- code/string/gsub-vs-tr.rb | 2 +- code/string/mutable_vs_immutable_strings.rb | 16 +++--- .../remove-extra-spaces-or-other-chars.rb | 2 +- ...art-string-checking-match-vs-start_with.rb | 6 ++- code/string/start_with-vs-substring-==.rb | 15 ++++-- code/string/sub-vs-chomp-vs-delete_suffix.rb | 4 +- 36 files changed, 266 insertions(+), 165 deletions(-) create mode 100644 code/general/raise-custom-vs-e2mmap.rb diff --git a/README.md b/README.md index cecf3ce9..8e0cd5b0 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ 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](code/general/raise-vs-e2mmap.rb) +##### `raise` vs `E2MM#Raise` for raising (and defining) exceptions [code](code/general/raise-vs-e2mmap.rb) [custom exception code](code/general/raise-custom-vs-e2mmap.rb) Ruby's [Exception2MessageMapper module](http://ruby-doc.org/stdlib-2.2.0/libdoc/e2mmap/rdoc/index.html) allows one to define and raise exceptions with predefined messages. @@ -155,7 +155,10 @@ Ruby exception: Kernel#raise Comparison: Ruby exception: Kernel#raise: 2570660.6 i/s Ruby exception: E2MM#Raise: 88268.9 i/s - 29.12x slower +``` +``` +$ ruby -v code/general/raise-custom-vs-e2mmap.rb ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] Warming up -------------------------------------- Custom exception: E2MM#Raise diff --git a/code/array/bsearch-vs-find.rb b/code/array/bsearch-vs-find.rb index 2984c1e5..cb705e94 100644 --- a/code/array/bsearch-vs-find.rb +++ b/code/array/bsearch-vs-find.rb @@ -1,9 +1,17 @@ require 'benchmark/ips' -data = [*0..100_000_000] +NUMBERS = [*0..100_000_000] + +def fast + NUMBERS.bsearch { |number| number > 77_777_777 } +end + +def slow + NUMBERS.find { |number| number > 77_777_777 } +end Benchmark.ips do |x| - x.report('find') { data.find { |number| number > 77_777_777 } } - x.report('bsearch') { data.bsearch { |number| number > 77_777_777 } } + x.report('bsearch') { fast } + x.report('find') { slow } x.compare! end diff --git a/code/array/insert-vs-unshift.rb b/code/array/insert-vs-unshift.rb index 992f24f6..b0ae4ed4 100644 --- a/code/array/insert-vs-unshift.rb +++ b/code/array/insert-vs-unshift.rb @@ -1,15 +1,17 @@ require 'benchmark/ips' -Benchmark.ips do |x| - x.report('Array#unshift') do - array = [] - 100_000.times { |i| array.unshift(i) } - end +def fast + array = [] + 100_000.times { |i| array.unshift(i) } +end - x.report('Array#insert') do - array = [] - 100_000.times { |i| array.insert(0, i) } - end +def slow + array = [] + 100_000.times { |i| array.insert(0, i) } +end +Benchmark.ips do |x| + x.report('Array#unshift') { fast } + x.report('Array#insert') { slow } x.compare! end diff --git a/code/array/length-vs-size-vs-count.rb b/code/array/length-vs-size-vs-count.rb index 6b7ff243..24ff2a91 100644 --- a/code/array/length-vs-size-vs-count.rb +++ b/code/array/length-vs-size-vs-count.rb @@ -2,9 +2,22 @@ ARRAY = [*1..100] +def fastest + ARRAY.length +end + +# Array#size is an alias of Array#length, so these two should tie. +def faster + ARRAY.size +end + +def slow + ARRAY.count +end + Benchmark.ips do |x| - x.report("Array#length") { ARRAY.length } - x.report("Array#size") { ARRAY.size } - x.report("Array#count") { ARRAY.count } + x.report("Array#length") { fastest } + x.report("Array#size") { faster } + x.report("Array#count") { slow } x.compare! end diff --git a/code/array/shuffle-first-vs-sample.rb b/code/array/shuffle-first-vs-sample.rb index 49a639b4..c838214b 100644 --- a/code/array/shuffle-first-vs-sample.rb +++ b/code/array/shuffle-first-vs-sample.rb @@ -11,7 +11,7 @@ def fast end Benchmark.ips do |x| - x.report('Array#shuffle.first') { slow } x.report('Array#sample') { fast } + x.report('Array#shuffle.first') { slow } x.compare! end diff --git a/code/enumerable/each-push-vs-map.rb b/code/enumerable/each-push-vs-map.rb index 78ed9fa2..d40e4163 100644 --- a/code/enumerable/each-push-vs-map.rb +++ b/code/enumerable/each-push-vs-map.rb @@ -12,7 +12,7 @@ def fast end Benchmark.ips do |x| - x.report('Array#each + push') { slow } x.report('Array#map') { fast } + x.report('Array#each + push') { slow } x.compare! end diff --git a/code/enumerable/each-vs-for-loop.rb b/code/enumerable/each-vs-for-loop.rb index 2b7c1ce3..f724dd1a 100644 --- a/code/enumerable/each-vs-for-loop.rb +++ b/code/enumerable/each-vs-for-loop.rb @@ -15,7 +15,7 @@ def fast end Benchmark.ips do |x| - x.report('For loop') { slow } x.report('#each') { fast } + x.report('For loop') { slow } x.compare! end diff --git a/code/enumerable/map-flatten-vs-flat_map.rb b/code/enumerable/map-flatten-vs-flat_map.rb index 25e0e79e..67f1fb0b 100644 --- a/code/enumerable/map-flatten-vs-flat_map.rb +++ b/code/enumerable/map-flatten-vs-flat_map.rb @@ -15,8 +15,8 @@ def fast end Benchmark.ips do |x| + x.report('Array#flat_map') { fast } x.report('Array#map.flatten(1)') { slow_flatten_1 } x.report('Array#map.flatten') { slow_flatten } - x.report('Array#flat_map') { fast } x.compare! end diff --git a/code/enumerable/reverse-each-vs-reverse_each.rb b/code/enumerable/reverse-each-vs-reverse_each.rb index 2ddf6a86..463da767 100644 --- a/code/enumerable/reverse-each-vs-reverse_each.rb +++ b/code/enumerable/reverse-each-vs-reverse_each.rb @@ -11,7 +11,7 @@ def fast end Benchmark.ips do |x| - x.report('Array#reverse.each') { slow } x.report('Array#reverse_each') { fast } + x.report('Array#reverse.each') { slow } x.compare! end diff --git a/code/enumerable/select-first-vs-detect.rb b/code/enumerable/select-first-vs-detect.rb index f535f69e..3fbef14d 100644 --- a/code/enumerable/select-first-vs-detect.rb +++ b/code/enumerable/select-first-vs-detect.rb @@ -11,7 +11,7 @@ def fast end Benchmark.ips do |x| - x.report('Enumerable#select.first') { slow } x.report('Enumerable#detect') { fast } + x.report('Enumerable#select.first') { slow } x.compare! end \ No newline at end of file diff --git a/code/general/attr-accessor-vs-getter-and-setter.rb b/code/general/attr-accessor-vs-getter-and-setter.rb index f6c442b3..6179c905 100644 --- a/code/general/attr-accessor-vs-getter-and-setter.rb +++ b/code/general/attr-accessor-vs-getter-and-setter.rb @@ -26,7 +26,7 @@ def fast end Benchmark.ips do |x| - x.report('getter_and_setter') { slow } x.report('attr_accessor') { fast } + x.report('getter_and_setter') { slow } x.compare! end diff --git a/code/general/begin-rescue-vs-respond-to.rb b/code/general/begin-rescue-vs-respond-to.rb index ba3bb89c..35369392 100644 --- a/code/general/begin-rescue-vs-respond-to.rb +++ b/code/general/begin-rescue-vs-respond-to.rb @@ -17,7 +17,7 @@ def fast end Benchmark.ips do |x| - x.report('begin...rescue') { slow } x.report('respond_to?') { fast } + x.report('begin...rescue') { slow } x.compare! end diff --git a/code/general/define_method-vs-module-eval.rb b/code/general/define_method-vs-module-eval.rb index 99dbfb18..0a229e8a 100644 --- a/code/general/define_method-vs-module-eval.rb +++ b/code/general/define_method-vs-module-eval.rb @@ -38,7 +38,7 @@ def slow Benchmark.ips do |x| - x.report("module_eval with string") { slow } x.report("define_method") { fast } + x.report("module_eval with string") { slow } x.compare! end diff --git a/code/general/raise-custom-vs-e2mmap.rb b/code/general/raise-custom-vs-e2mmap.rb new file mode 100644 index 00000000..eaf5ab30 --- /dev/null +++ b/code/general/raise-custom-vs-e2mmap.rb @@ -0,0 +1,45 @@ +require 'benchmark/ips' +require 'e2mmap' + +# Defines and raises an exception class of its own (FooError). +# For an exception class Ruby already defines, see raise-vs-e2mmap.rb. + +class WithE2MM + extend Exception2MessageMapper + + def_exception :FooError, 'foo: %s' + + def self.raise_user_defined + Raise FooError, 'bar!' + end +end + +class WithoutE2MM + FooError = Class.new(StandardError) + + def self.raise_user_defined + raise FooError, 'foo: bar!' + end +end + +def fast + begin + WithoutE2MM.raise_user_defined + rescue + 'fast ruby' + end +end + +def slow + begin + WithE2MM.raise_user_defined + rescue + 'fast ruby' + end +end + +Benchmark.ips do |x| + x.report('Custom exception: Kernel#raise') { fast } + x.report('Custom exception: E2MM#Raise') { slow } + x.compare! +end diff --git a/code/general/raise-vs-e2mmap.rb b/code/general/raise-vs-e2mmap.rb index 2d8ee4ee..62b0cd04 100644 --- a/code/general/raise-vs-e2mmap.rb +++ b/code/general/raise-vs-e2mmap.rb @@ -1,42 +1,26 @@ require 'benchmark/ips' require 'e2mmap' +# Raises an exception class Ruby already defines (TypeError). +# For an exception class defined in the file, see raise-custom-vs-e2mmap.rb. + class WithE2MM extend Exception2MessageMapper def_e2message TypeError, 'argument must be a %s' - def_exception :FooError, 'foo: %s' def self.raise_ruby_defined Raise TypeError, 'Hash' end - - def self.raise_user_defined - Raise FooError, 'bar!' - end end class WithoutE2MM - FooError = Class.new(StandardError) - def self.raise_ruby_defined raise TypeError, 'argument must be a Hash' end - - def self.raise_user_defined - raise FooError, 'foo: bar!' - end -end - -def slow_ruby_defined - begin - WithE2MM.raise_ruby_defined - rescue - 'fast ruby' - end end -def fast_ruby_defined +def fast begin WithoutE2MM.raise_ruby_defined rescue @@ -44,30 +28,16 @@ def fast_ruby_defined end end -def slow_user_defined - begin - WithE2MM.raise_user_defined - rescue - 'fast ruby' - end -end - -def fast_user_defined +def slow begin - WithoutE2MM.raise_user_defined + WithE2MM.raise_ruby_defined rescue 'fast ruby' end end Benchmark.ips do |x| - x.report('Ruby exception: E2MM#Raise') { slow_ruby_defined } - x.report('Ruby exception: Kernel#raise') { fast_ruby_defined } - x.compare! -end - -Benchmark.ips do |x| - x.report('Custom exception: E2MM#Raise') { slow_user_defined } - x.report('Custom exception: Kernel#raise') { fast_user_defined } + x.report('Ruby exception: Kernel#raise') { fast } + x.report('Ruby exception: E2MM#Raise') { slow } x.compare! end diff --git a/code/hash/dig-vs-[]-vs-fetch.rb b/code/hash/dig-vs-[]-vs-fetch.rb index 35bfe5fb..43d95abe 100644 --- a/code/hash/dig-vs-[]-vs-fetch.rb +++ b/code/hash/dig-vs-[]-vs-fetch.rb @@ -1,33 +1,40 @@ require "benchmark/ips" -h = { a: { b: { c: { d: { e: "foo" } } } } } +HASH = { a: { b: { c: { d: { e: "foo" } } } } } -Benchmark.ips do |x| - if RUBY_VERSION >= "2.3.0" - x.report "Hash#dig" do - h.dig(:a, :b, :c, :d, :e) - end - end +# Plain Hash#[] is the fastest, but raises NoMethodError when a level is missing. +# Hash#dig returns nil instead, which is why it is the readable choice for nested hashes, at a small cost. +def fastest + HASH[:a][:b][:c][:d][:e] +end - x.report "Hash#[]" do - h[:a][:b][:c][:d][:e] - end +def faster + ((((HASH[:a] || {})[:b] || {})[:c] || {})[:d] || {})[:e] +end - x.report "Hash#[] ||" do - ((((h[:a] || {})[:b] || {})[:c] || {})[:d] || {})[:e] - end +def fast + HASH.dig(:a, :b, :c, :d, :e) +end - x.report "Hash#[] &&" do - h[:a] && h[:a][:b] && h[:a][:b][:c] && h[:a][:b][:c][:d] && h[:a][:b][:c][:d][:e] - end +def slow + HASH.fetch(:a).fetch(:b).fetch(:c).fetch(:d).fetch(:e) +end - x.report "Hash#fetch" do - h.fetch(:a).fetch(:b).fetch(:c).fetch(:d).fetch(:e) - end +# These last two swap places across Rubies; this one is faster on 3.2 and newer. +def slower + HASH[:a] && HASH[:a][:b] && HASH[:a][:b][:c] && HASH[:a][:b][:c][:d] && HASH[:a][:b][:c][:d][:e] +end - x.report "Hash#fetch fallback" do - h.fetch(:a, {}).fetch(:b, {}).fetch(:c, {}).fetch(:d, {}).fetch(:e, nil) - end +def slowest + HASH.fetch(:a, {}).fetch(:b, {}).fetch(:c, {}).fetch(:d, {}).fetch(:e, nil) +end +Benchmark.ips do |x| + x.report("Hash#[]") { fastest } + x.report("Hash#[] ||") { faster } + x.report("Hash#dig") { fast } if RUBY_VERSION >= "2.3.0" + x.report("Hash#fetch") { slow } + x.report("Hash#[] &&") { slower } + x.report("Hash#fetch fallback") { slowest } x.compare! end diff --git a/code/hash/fetch-vs-fetch-with-block.rb b/code/hash/fetch-vs-fetch-with-block.rb index 68badccf..c544d2b5 100644 --- a/code/hash/fetch-vs-fetch-with-block.rb +++ b/code/hash/fetch-vs-fetch-with-block.rb @@ -3,9 +3,25 @@ HASH = { writing: :fast_ruby } DEFAULT = "fast ruby" +# The default is written differently on purpose: +# a string argument is built on every call, even when the key exists. +# The block builds it only when the key is missing, +# and the constant is built once, so those two are close. +def fastest + HASH.fetch(:writing, DEFAULT) +end + +def faster + HASH.fetch(:writing) { "fast ruby" } +end + +def slow + HASH.fetch(:writing, "fast ruby") +end + Benchmark.ips do |x| - x.report("Hash#fetch + const") { HASH.fetch(:writing, DEFAULT) } - x.report("Hash#fetch + block") { HASH.fetch(:writing) { "fast ruby" } } - x.report("Hash#fetch + arg") { HASH.fetch(:writing, "fast ruby") } + x.report("Hash#fetch + const") { fastest } + x.report("Hash#fetch + block") { faster } + x.report("Hash#fetch + arg") { slow } x.compare! end diff --git a/code/hash/keys-each-vs-each_key.rb b/code/hash/keys-each-vs-each_key.rb index fd3dd6a6..24ecf5fc 100644 --- a/code/hash/keys-each-vs-each_key.rb +++ b/code/hash/keys-each-vs-each_key.rb @@ -48,7 +48,7 @@ def fast end Benchmark.ips do |x| - x.report('Hash#keys.each') { slow } x.report('Hash#each_key') { fast } + x.report('Hash#keys.each') { slow } x.compare! end diff --git a/code/hash/keys-include-vs-key.rb b/code/hash/keys-include-vs-key.rb index 1dbd4cbe..1ff53efb 100644 --- a/code/hash/keys-include-vs-key.rb +++ b/code/hash/keys-include-vs-key.rb @@ -1,18 +1,18 @@ require "benchmark/ips" -HASH = Hash[*("a".."zzz").to_a.shuffle] -KEY = "zz" +HASH = Hash[*("a".."zzz").to_a] # 9139 pairs, same order every run +KEY = HASH.keys[HASH.size / 2] # always found halfway through -def key_fast +def fast HASH.key? KEY end -def key_slow +def slow HASH.keys.include? KEY end Benchmark.ips do |x| - x.report("Hash#keys.include?") { key_slow } - x.report("Hash#key?") { key_fast } + x.report("Hash#key?") { fast } + x.report("Hash#keys.include?") { slow } x.compare! end diff --git a/code/hash/merge-bang-vs-[]=.rb b/code/hash/merge-bang-vs-[]=.rb index 16b099fa..11884ceb 100644 --- a/code/hash/merge-bang-vs-[]=.rb +++ b/code/hash/merge-bang-vs-[]=.rb @@ -15,7 +15,7 @@ def fast end Benchmark.ips do |x| - x.report('Hash#merge!') { slow } x.report('Hash#[]=') { fast } + x.report('Hash#merge!') { slow } x.compare! end diff --git a/code/hash/merge-vs-merge-bang.rb b/code/hash/merge-vs-merge-bang.rb index df501d5b..e56f85d1 100644 --- a/code/hash/merge-vs-merge-bang.rb +++ b/code/hash/merge-vs-merge-bang.rb @@ -15,7 +15,7 @@ def fast end Benchmark.ips do |x| - x.report('Hash#merge') { slow } x.report('Hash#merge!') { fast } + x.report('Hash#merge') { slow } x.compare! end diff --git a/code/hash/values-include-vs-value.rb b/code/hash/values-include-vs-value.rb index ae4342dc..a72ba128 100644 --- a/code/hash/values-include-vs-value.rb +++ b/code/hash/values-include-vs-value.rb @@ -1,18 +1,20 @@ require "benchmark/ips" -HASH = Hash[*("a".."zzz").to_a.shuffle] -VALUE = "zz" +HASH = Hash[*("a".."zzz").to_a] # 9139 pairs, same order every run +VALUE = HASH.values[HASH.size / 2] # always found halfway through -def value_fast +# Hash#value? scans the hash directly; Hash#values.include? first copies every value into a new array. +# On CRuby the two tie at this position; Hash#value? wins on JRuby and TruffleRuby. +def fast HASH.value? VALUE end -def value_slow +def slow HASH.values.include? VALUE end Benchmark.ips do |x| - x.report("Hash#values.include?") { value_slow } - x.report("Hash#value?") { value_fast } + x.report("Hash#value?") { fast } + x.report("Hash#values.include?") { slow } x.compare! end diff --git a/code/proc-and-block/block-vs-to_proc.rb b/code/proc-and-block/block-vs-to_proc.rb index 10845e14..70d68e7f 100644 --- a/code/proc-and-block/block-vs-to_proc.rb +++ b/code/proc-and-block/block-vs-to_proc.rb @@ -11,7 +11,7 @@ def fast end Benchmark.ips do |x| - x.report('Block') { slow } x.report('Symbol#to_proc') { fast } + x.report('Block') { slow } x.compare! end diff --git a/code/proc-and-block/proc-call-vs-yield.rb b/code/proc-and-block/proc-call-vs-yield.rb index 3e95968c..f595f5ca 100644 --- a/code/proc-and-block/proc-call-vs-yield.rb +++ b/code/proc-and-block/proc-call-vs-yield.rb @@ -17,9 +17,9 @@ def fast end Benchmark.ips do |x| + x.report('yield') { fast { 1 + 1 } } x.report('block.call') { slow { 1 + 1 } } x.report('block + yield') { slow2 { 1 + 1 } } x.report('unused block') { slow3 { 1 + 1 } } - x.report('yield') { fast { 1 + 1 } } x.compare! end diff --git a/code/range/cover-vs-include.rb b/code/range/cover-vs-include.rb index 8d23c1b4..bf144cf9 100644 --- a/code/range/cover-vs-include.rb +++ b/code/range/cover-vs-include.rb @@ -5,12 +5,33 @@ END_OF_JULY = Date.new(2015, 7, 31) DAY_IN_JULY = Date.new(2015, 7, 15) +# between? and plain compare only compare the dates, without building a Range, so they beat range#cover?. +def fastest + DAY_IN_JULY.between?(BEGIN_OF_JULY, END_OF_JULY) +end + +def faster + BEGIN_OF_JULY < DAY_IN_JULY && DAY_IN_JULY < END_OF_JULY +end + +# cover? only compares with the ends of the range; include? and member? walk it, since Date is not numeric. +def fast + (BEGIN_OF_JULY..END_OF_JULY).cover? DAY_IN_JULY +end + +def slow + (BEGIN_OF_JULY..END_OF_JULY).include? DAY_IN_JULY +end + +def slower + (BEGIN_OF_JULY..END_OF_JULY).member? DAY_IN_JULY +end + Benchmark.ips do |x| - x.report('range#cover?') { (BEGIN_OF_JULY..END_OF_JULY).cover? DAY_IN_JULY } - x.report('range#include?') { (BEGIN_OF_JULY..END_OF_JULY).include? DAY_IN_JULY } - x.report('range#member?') { (BEGIN_OF_JULY..END_OF_JULY).member? DAY_IN_JULY } - x.report('plain compare') { BEGIN_OF_JULY < DAY_IN_JULY && DAY_IN_JULY < END_OF_JULY } - x.report('value.between?') { DAY_IN_JULY.between?(BEGIN_OF_JULY, END_OF_JULY) } - + x.report('value.between?') { fastest } + x.report('plain compare') { faster } + x.report('range#cover?') { fast } + x.report('range#include?') { slow } + x.report('range#member?') { slower } x.compare! end diff --git a/code/string/casecmp-vs-downcase-==.rb b/code/string/casecmp-vs-downcase-==.rb index be46803f..781fa426 100644 --- a/code/string/casecmp-vs-downcase-==.rb +++ b/code/string/casecmp-vs-downcase-==.rb @@ -15,8 +15,8 @@ def fast end Benchmark.ips do |x| - x.report("String#casecmp?") { slowest } if RUBY_VERSION >= "2.4.0".freeze - x.report('String#downcase + ==') { slow } x.report('String#casecmp') { fast } + x.report('String#downcase + ==') { slow } + x.report("String#casecmp?") { slowest } if RUBY_VERSION >= "2.4.0".freeze x.compare! end diff --git a/code/string/concatenation.rb b/code/string/concatenation.rb index e249a307..e0190a07 100644 --- a/code/string/concatenation.rb +++ b/code/string/concatenation.rb @@ -1,34 +1,38 @@ require 'benchmark/ips' -# 2 + 1 = 3 object -def slow_plus - 'foo' + 'bar' +# Object counts are per call, measured on Ruby 3.4. + +# 1 object: the result. +# Both of these are built from literals when the file is parsed, so they are close; which one wins varies by Ruby version. +def fastest + "#{'foo'}#{'bar'}" end -# 2 + 1 = 3 object -def slow_concat - 'foo'.concat 'bar' +# 1 object: the result. +def faster + 'foo' 'bar' end -# 2 + 1 = 3 object -def slow_append +# 2 objects: 'foo' and 'bar'; 'foo' is changed in place. +def slow 'foo' << 'bar' end -# 1 object -def fast - 'foo' 'bar' +# 2 objects: 'foo' and 'bar'; 'foo' is changed in place. +def slower + 'foo'.concat 'bar' end -def fast_interpolation - "#{'foo'}#{'bar'}" +# 3 objects: 'foo', 'bar' and the new result. +def slowest + 'foo' + 'bar' end Benchmark.ips do |x| - x.report('String#+') { slow_plus } - x.report('String#concat') { slow_concat } - x.report('String#append') { slow_append } - x.report('"foo" "bar"') { fast } - x.report('"#{\'foo\'}#{\'bar\'}"') { fast_interpolation } + x.report('"#{\'foo\'}#{\'bar\'}"') { fastest } + x.report('"foo" "bar"') { faster } + x.report('String#append') { slow } + x.report('String#concat') { slower } + x.report('String#+') { slowest } x.compare! end diff --git a/code/string/end-string-checking-match-vs-end_with.rb b/code/string/end-string-checking-match-vs-end_with.rb index e95d87ab..fffdf827 100644 --- a/code/string/end-string-checking-match-vs-end_with.rb +++ b/code/string/end-string-checking-match-vs-end_with.rb @@ -15,8 +15,8 @@ def fast end Benchmark.ips do |x| - x.report('String#=~') { slower } - x.report('String#match?') { slow } if RUBY_VERSION >= "2.4.0".freeze x.report('String#end_with?') { fast } + x.report('String#match?') { slow } if RUBY_VERSION >= "2.4.0".freeze + x.report('String#=~') { slower } x.compare! end diff --git a/code/string/gsub-vs-sub.rb b/code/string/gsub-vs-sub.rb index 1d0804dd..8bc4d97f 100644 --- a/code/string/gsub-vs-sub.rb +++ b/code/string/gsub-vs-sub.rb @@ -17,8 +17,8 @@ def fastest end Benchmark.ips do |x| - x.report('String#gsub') { slow } - x.report('String#sub') { fast } x.report('String#dup["string"]=') { fastest } + x.report('String#sub') { fast } + x.report('String#gsub') { slow } x.compare! end diff --git a/code/string/gsub-vs-tr-vs-delete.rb b/code/string/gsub-vs-tr-vs-delete.rb index 6afd0393..1b137afc 100644 --- a/code/string/gsub-vs-tr-vs-delete.rb +++ b/code/string/gsub-vs-tr-vs-delete.rb @@ -3,26 +3,27 @@ WORDS = 'writing fast ruby' SPACE = ' ' -def use_gsub - WORDS.gsub(' ', '') +# The same call as `faster`, but ' ' is allocated once instead of on every call. +def fastest + WORDS.delete(SPACE) end -def use_tr - WORDS.tr(' ', '') +def faster + WORDS.delete(' ') end -def use_delete - WORDS.delete(' ') +def fast + WORDS.tr(' ', '') end -def use_delete_const - WORDS.delete(SPACE) +def slow + WORDS.gsub(' ', '') end Benchmark.ips do |x| - x.report('String#gsub') { use_gsub } - x.report('String#tr') { use_tr } - x.report('String#delete') { use_delete } - x.report('String#delete const') { use_delete_const } + x.report('String#delete const') { fastest } + x.report('String#delete') { faster } + x.report('String#tr') { fast } + x.report('String#gsub') { slow } x.compare! end diff --git a/code/string/gsub-vs-tr.rb b/code/string/gsub-vs-tr.rb index 6edbeee2..9c2f306b 100644 --- a/code/string/gsub-vs-tr.rb +++ b/code/string/gsub-vs-tr.rb @@ -11,7 +11,7 @@ def fast end Benchmark.ips do |x| - x.report('String#gsub') { slow } x.report('String#tr') { fast } + x.report('String#gsub') { slow } x.compare! end diff --git a/code/string/mutable_vs_immutable_strings.rb b/code/string/mutable_vs_immutable_strings.rb index d02a739a..3bdd3b96 100644 --- a/code/string/mutable_vs_immutable_strings.rb +++ b/code/string/mutable_vs_immutable_strings.rb @@ -1,17 +1,17 @@ require "benchmark/ips" -# Allocates new string over and over again -def without_freeze - "To freeze or not to freeze" -end - # Keeps and reuses shared string -def with_feeze +def fast "To freeze or not to freeze".freeze end +# Allocates new string over and over again +def slow + "To freeze or not to freeze" +end + Benchmark.ips do |x| - x.report("Without Freeze") { without_freeze } - x.report("With Freeze") { with_feeze } + x.report("With Freeze") { fast } + x.report("Without Freeze") { slow } x.compare! end diff --git a/code/string/remove-extra-spaces-or-other-chars.rb b/code/string/remove-extra-spaces-or-other-chars.rb index 32c9c5c4..c28ecefc 100644 --- a/code/string/remove-extra-spaces-or-other-chars.rb +++ b/code/string/remove-extra-spaces-or-other-chars.rb @@ -19,7 +19,7 @@ def fast end Benchmark.ips do |x| - x.report('String#gsub/regex+/') { slow } x.report('String#squeeze') { fast } + x.report('String#gsub/regex+/') { slow } x.compare! end diff --git a/code/string/start-string-checking-match-vs-start_with.rb b/code/string/start-string-checking-match-vs-start_with.rb index a58eb35d..e5a9ba6e 100644 --- a/code/string/start-string-checking-match-vs-start_with.rb +++ b/code/string/start-string-checking-match-vs-start_with.rb @@ -10,13 +10,15 @@ def slow SLUG.match?(/^test_/) end +# String#match? wins on Ruby 3.4 (with or without YJIT). +# start_with? wins on the other Rubies CI runs. def fast SLUG.start_with?('test_') end Benchmark.ips do |x| - x.report('String#=~') { slower } - x.report('String#match?') { slow } if RUBY_VERSION >= "2.4.0".freeze x.report('String#start_with?') { fast } + x.report('String#match?') { slow } if RUBY_VERSION >= "2.4.0".freeze + x.report('String#=~') { slower } x.compare! end diff --git a/code/string/start_with-vs-substring-==.rb b/code/string/start_with-vs-substring-==.rb index 32d8c180..15723f5d 100755 --- a/code/string/start_with-vs-substring-==.rb +++ b/code/string/start_with-vs-substring-==.rb @@ -24,10 +24,17 @@ "STRINGS[#{i}][0...PREFIX.length].eql?(PREFIX)" end.join(";") +# Each check is written out for all 10 strings (not looped), so only the checks are measured. +# The methods are defined from those strings. +eval "def fastest\n#{START_WITH}\nend" +eval "def faster\n#{EQL_USING_LENGTH}\nend" +eval "def fast\n#{EQL_USING_RANGE_PREALLOC}\nend" +eval "def slow\n#{EQL_USING_RANGE}\nend" + Benchmark.ips do |x| - x.report("String#start_with?", START_WITH) - x.report("String#[0, n] ==", EQL_USING_LENGTH) - x.report("String#[RANGE] ==", EQL_USING_RANGE_PREALLOC) - x.report("String#[0...n] ==", EQL_USING_RANGE) + x.report("String#start_with?") { fastest } + x.report("String#[0, n] ==") { faster } + x.report("String#[RANGE] ==") { fast } + x.report("String#[0...n] ==") { slow } x.compare! end diff --git a/code/string/sub-vs-chomp-vs-delete_suffix.rb b/code/string/sub-vs-chomp-vs-delete_suffix.rb index 6ecfcf9f..a5ae925f 100644 --- a/code/string/sub-vs-chomp-vs-delete_suffix.rb +++ b/code/string/sub-vs-chomp-vs-delete_suffix.rb @@ -15,8 +15,8 @@ def faster end Benchmark.ips do |x| - x.report('String#sub') { slow } - x.report("String#chomp") { fast } x.report("String#delete_suffix") { faster } if RUBY_VERSION >= '2.5.0' + x.report("String#chomp") { fast } + x.report('String#sub') { slow } x.compare! end From 1bf7bcb9d24427b7ab5a54f0e1bd353ff225bb12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Thu, 24 Sep 2026 11:06:21 -0600 Subject: [PATCH 2/4] Step 2: Lint that each benchmark names one winner, first A fourth lint rule: in every Benchmark.ips block, exactly one report calls `fastest` (else `faster`, else `fast`), and it is the first report. The claim then comes from the code, not from README prose, and the results site can check it against every run. The lint job already runs this script and benchmarks-ok requires it, so the workflow does not change. CONTRIBUTING explains the naming with length-vs-size-vs-count.rb. --- .github/scripts/lint-benchmarks.rb | 36 ++++++++++++++++++++++++++---- CONTRIBUTING.md | 31 ++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/.github/scripts/lint-benchmarks.rb b/.github/scripts/lint-benchmarks.rb index 48c166f2..7b2d9fa6 100644 --- a/.github/scripts/lint-benchmarks.rb +++ b/.github/scripts/lint-benchmarks.rb @@ -6,18 +6,21 @@ # so every file uses the default # - no Benchmark.ips sits inside a method that never runs from the top of the file, # which would benchmark nothing at all +# - every Benchmark.ips block states which report should win: exactly one +# report calls `fastest` (else `faster`, else `fast`), and it comes first # # Usage: ruby .github/scripts/lint-benchmarks.rb [files...] (needs Ruby 3.3+) require "prism" -# Calls named `name` that have a block, not looking inside the ones found. -def find_calls(node, name, found = []) +# Calls named `name`, not looking inside the ones found. Only calls with a +# block, unless `with_block: false` (reports can be a code string: x.report(label, code)). +def find_calls(node, name, with_block: true, found: []) return found unless node - if node.is_a?(Prism::CallNode) && node.name == name && node.block + if node.is_a?(Prism::CallNode) && node.name == name && (node.block || !with_block) found << node else - node.compact_child_nodes.each { |child| find_calls(child, name, found) } + node.compact_child_nodes.each { |child| find_calls(child, name, with_block: with_block, found: found) } end found end @@ -29,6 +32,28 @@ def any_call?(node, &test) node.compact_child_nodes.any? { |child| any_call?(child, &test) } end +CLAIM_METHODS = %i[fastest faster fast].freeze + +# Method names called under `node` without a receiver. +def called_names(node, names = []) + return names unless node + + names << node.name if node.is_a?(Prism::CallNode) && node.receiver.nil? + node.compact_child_nodes.each { |child| called_names(child, names) } + names +end + +def claim_problem(ips) + reports = find_calls(ips.block, :report, with_block: false).map { |r| called_names(r.block) } + top = CLAIM_METHODS.find { |name| reports.any? { |calls| calls.include?(name) } } + return "no claim. Wrap each report in a method and name the winner's `fast` (or `faster`, `fastest`)" unless top + + claimed = reports.count { |calls| calls.include?(top) } + return "#{claimed} reports call `#{top}`. Name only the winner `#{top}`" if claimed > 1 + + "the report calling `#{top}` must come first" unless reports.first.include?(top) +end + TIMING_KEYS = %w[time warmup].freeze # x.time = 20, x.warmup = 5, or x.config with a time or warmup key, @@ -159,6 +184,9 @@ def lint(file) name = owner.keys.last.delete_prefix("#") problems << "#{where}: inside `def #{name}`, which never runs from the top of the file" end + + claim = claim_problem(ips) + problems << "#{where}: #{claim}" if claim end problems end diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f84750a..4125e0b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,36 @@ Keep that shape: end every `Benchmark.ips` block with `x.compare!`, keep the default timing (no `Benchmark.ips(20)`, `x.time = ...` or `x.config(time: ...)`), so every entry is measured the same way, and make sure the file actually calls `Benchmark.ips` when it runs -(not only inside a method nothing calls). CI checks all three. +(not only inside a method nothing calls). CI checks these, and the naming below. + +The method names say which report should win. +CI checks that exactly one report is named the winner and that it comes first. +Wrap every report in a method, so they all pay the same call cost. +Name them by rank and list the winner first: `fastest`, `faster`, `fast` at the top, then `slow`, `slower`, `slowest`. +The names are relative: `slow` only means slower than `fast`. +When a ranking could look odd, say why in one line, like in `code/array/length-vs-size-vs-count.rb`: + +```ruby +def fastest + ARRAY.length +end + +# Array#size is an alias of Array#length, so these two should tie. +def faster + ARRAY.size +end + +def slow + ARRAY.count +end + +Benchmark.ips do |x| + x.report("Array#length") { fastest } + x.report("Array#size") { faster } + x.report("Array#count") { slow } + x.compare! +end +``` Run your result: From 07b844c6675bdae161651476b7731cec2b160957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Thu, 24 Sep 2026 11:41:17 -0600 Subject: [PATCH 3/4] Step 3: Name every report by rank and define methods in that order Finish the naming from Step 1 across all files, so each one reads top to bottom as its advice. - Every report now calls a rank-named method: avg, slow_dup, even_slower, slow2 and slow_1 to slow_5 are gone. Names follow the order in CI run 35927028164. - Names grow outward from the line between fast and slow, never skipping a step: the recommended side uses fast, faster, fastest, the other side slow, slower, slowest. The names give the order, not the size of the gap, which changes from one Ruby to the next. - Methods are defined in the same order as the reports. - proc-call-vs-yield.rb drops the "unused block" report: it never called the block, so it did less work than the others and won on 17 of 26 jobs. - One-line comments where a ranking depends on the Ruby or should tie (Range#member? is an alias of Range#include?). - inheritance-check.rb explains its two raise lines, and inject-symbol-vs-block.rb drops a require "rubygems" only Ruby 1.8 needed. - CONTRIBUTING's naming text and example follow the same rule. --- CONTRIBUTING.md | 11 ++++--- code/array/length-vs-size-vs-count.rb | 8 ++--- code/array/shuffle-first-vs-sample.rb | 8 ++--- code/enumerable/each-push-vs-map.rb | 8 ++--- code/enumerable/each-vs-for-loop.rb | 8 ++--- code/enumerable/inject-symbol-vs-block.rb | 7 ++-- code/enumerable/map-flatten-vs-flat_map.rb | 18 +++++----- .../reverse-each-vs-reverse_each.rb | 8 ++--- code/enumerable/select-first-vs-detect.rb | 12 +++---- code/enumerable/sort-vs-sort_by.rb | 10 +++--- .../attr-accessor-vs-getter-and-setter.rb | 11 +++---- code/general/begin-rescue-vs-respond-to.rb | 12 +++---- code/general/define_method-vs-module-eval.rb | 1 - code/general/format-vs-round-and-to-s.rb | 8 ++--- code/general/inheritance-check.rb | 6 ++-- code/hash/fetch-vs-fetch-with-block.rb | 8 ++--- code/hash/keys-each-vs-each_key.rb | 8 ++--- code/hash/merge-bang-vs-[]=.rb | 8 ++--- .../merge-bang-vs-merge-vs-dup-merge-bang.rb | 4 +-- code/hash/merge-vs-merge-bang.rb | 8 ++--- code/method/call-vs-send-vs-method_missing.rb | 8 ++--- code/proc-and-block/block-vs-to_proc.rb | 8 ++--- code/proc-and-block/proc-call-vs-yield.rb | 21 +++++------- code/range/cover-vs-include.rb | 1 + code/string/===-vs-=~-vs-match.rb | 19 ++++++----- code/string/casecmp-vs-downcase-==.rb | 10 +++--- code/string/concatenation.rb | 8 ++--- .../end-string-checking-match-vs-end_with.rb | 8 ++--- code/string/gsub-vs-sub.rb | 14 ++++---- code/string/gsub-vs-tr.rb | 8 ++--- .../remove-extra-spaces-or-other-chars.rb | 8 ++--- ...art-string-checking-match-vs-start_with.rb | 12 +++---- code/string/sub!-vs-gsub!-vs-[]=.rb | 33 +++++++++---------- code/string/sub-vs-chomp-vs-delete_suffix.rb | 8 ++--- 34 files changed, 167 insertions(+), 171 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4125e0b4..e4ee7dbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,17 +44,18 @@ the file actually calls `Benchmark.ips` when it runs The method names say which report should win. CI checks that exactly one report is named the winner and that it comes first. Wrap every report in a method, so they all pay the same call cost. -Name them by rank and list the winner first: `fastest`, `faster`, `fast` at the top, then `slow`, `slower`, `slowest`. +Name them by rank and list the winner first. +The names grow outward from the line between fast and slow: the recommended side uses `fast`, then `faster`, then `fastest`; the other side uses `slow`, then `slower`, then `slowest`. The names are relative: `slow` only means slower than `fast`. When a ranking could look odd, say why in one line, like in `code/array/length-vs-size-vs-count.rb`: ```ruby -def fastest +def faster ARRAY.length end # Array#size is an alias of Array#length, so these two should tie. -def faster +def fast ARRAY.size end @@ -63,8 +64,8 @@ def slow end Benchmark.ips do |x| - x.report("Array#length") { fastest } - x.report("Array#size") { faster } + x.report("Array#length") { faster } + x.report("Array#size") { fast } x.report("Array#count") { slow } x.compare! end diff --git a/code/array/length-vs-size-vs-count.rb b/code/array/length-vs-size-vs-count.rb index 24ff2a91..47e3b810 100644 --- a/code/array/length-vs-size-vs-count.rb +++ b/code/array/length-vs-size-vs-count.rb @@ -2,12 +2,12 @@ ARRAY = [*1..100] -def fastest +def faster ARRAY.length end # Array#size is an alias of Array#length, so these two should tie. -def faster +def fast ARRAY.size end @@ -16,8 +16,8 @@ def slow end Benchmark.ips do |x| - x.report("Array#length") { fastest } - x.report("Array#size") { faster } + x.report("Array#length") { faster } + x.report("Array#size") { fast } x.report("Array#count") { slow } x.compare! end diff --git a/code/array/shuffle-first-vs-sample.rb b/code/array/shuffle-first-vs-sample.rb index c838214b..374e1b42 100644 --- a/code/array/shuffle-first-vs-sample.rb +++ b/code/array/shuffle-first-vs-sample.rb @@ -2,14 +2,14 @@ ARRAY = [*1..100] -def slow - ARRAY.shuffle.first -end - def fast ARRAY.sample end +def slow + ARRAY.shuffle.first +end + Benchmark.ips do |x| x.report('Array#sample') { fast } x.report('Array#shuffle.first') { slow } diff --git a/code/enumerable/each-push-vs-map.rb b/code/enumerable/each-push-vs-map.rb index d40e4163..e5a6f541 100644 --- a/code/enumerable/each-push-vs-map.rb +++ b/code/enumerable/each-push-vs-map.rb @@ -2,15 +2,15 @@ ARRAY = (1..100).to_a +def fast + ARRAY.map { |i| i } +end + def slow array = [] ARRAY.each { |i| array.push i } end -def fast - ARRAY.map { |i| i } -end - Benchmark.ips do |x| x.report('Array#map') { fast } x.report('Array#each + push') { slow } diff --git a/code/enumerable/each-vs-for-loop.rb b/code/enumerable/each-vs-for-loop.rb index f724dd1a..7fd611ba 100644 --- a/code/enumerable/each-vs-for-loop.rb +++ b/code/enumerable/each-vs-for-loop.rb @@ -2,14 +2,14 @@ ARRAY = [*1..100] -def slow - for number in ARRAY do +def fast + ARRAY.each do |number| number end end -def fast - ARRAY.each do |number| +def slow + for number in ARRAY do number end end diff --git a/code/enumerable/inject-symbol-vs-block.rb b/code/enumerable/inject-symbol-vs-block.rb index c261f718..3b37bf8d 100644 --- a/code/enumerable/inject-symbol-vs-block.rb +++ b/code/enumerable/inject-symbol-vs-block.rb @@ -1,12 +1,12 @@ -require "rubygems" require "benchmark/ips" ARRAY = (1..1000).to_a -def fastest +def faster ARRAY.inject(:+) end +# Symbol#to_proc beats the block on plain CRuby up to 3.4; the block wins with YJIT or ZJIT, on 4.0 and newer, and on JRuby. def fast ARRAY.inject(&:+) end @@ -16,9 +16,8 @@ def slow end Benchmark.ips do |x| - x.report('inject symbol') { fastest } + x.report('inject symbol') { faster } x.report('inject to_proc') { fast } x.report('inject block') { slow } - x.compare! end diff --git a/code/enumerable/map-flatten-vs-flat_map.rb b/code/enumerable/map-flatten-vs-flat_map.rb index 67f1fb0b..f29a6e17 100644 --- a/code/enumerable/map-flatten-vs-flat_map.rb +++ b/code/enumerable/map-flatten-vs-flat_map.rb @@ -2,21 +2,21 @@ ARRAY = (1..100).to_a -def slow_flatten_1 - ARRAY.map { |e| [e, e] }.flatten(1) +def fast + ARRAY.flat_map { |e| [e, e] } end -def slow_flatten - ARRAY.map { |e| [e, e] }.flatten +def slow + ARRAY.map { |e| [e, e] }.flatten(1) end -def fast - ARRAY.flat_map { |e| [e, e] } +def slower + ARRAY.map { |e| [e, e] }.flatten end Benchmark.ips do |x| - x.report('Array#flat_map') { fast } - x.report('Array#map.flatten(1)') { slow_flatten_1 } - x.report('Array#map.flatten') { slow_flatten } + x.report('Array#flat_map') { fast } + x.report('Array#map.flatten(1)') { slow } + x.report('Array#map.flatten') { slower } x.compare! end diff --git a/code/enumerable/reverse-each-vs-reverse_each.rb b/code/enumerable/reverse-each-vs-reverse_each.rb index 463da767..f1f15ae2 100644 --- a/code/enumerable/reverse-each-vs-reverse_each.rb +++ b/code/enumerable/reverse-each-vs-reverse_each.rb @@ -2,14 +2,14 @@ ARRAY = (1..100).to_a -def slow - ARRAY.reverse.each{|x| x} -end - def fast ARRAY.reverse_each{|x| x} end +def slow + ARRAY.reverse.each{|x| x} +end + Benchmark.ips do |x| x.report('Array#reverse_each') { fast } x.report('Array#reverse.each') { slow } diff --git a/code/enumerable/select-first-vs-detect.rb b/code/enumerable/select-first-vs-detect.rb index 3fbef14d..7105290e 100644 --- a/code/enumerable/select-first-vs-detect.rb +++ b/code/enumerable/select-first-vs-detect.rb @@ -2,16 +2,16 @@ ARRAY = [*1..100] -def slow - ARRAY.select { |x| x.eql?(15) }.first -end - def fast ARRAY.detect { |x| x.eql?(15) } end +def slow + ARRAY.select { |x| x.eql?(15) }.first +end + Benchmark.ips do |x| - x.report('Enumerable#detect') { fast } + x.report('Enumerable#detect') { fast } x.report('Enumerable#select.first') { slow } x.compare! -end \ No newline at end of file +end diff --git a/code/enumerable/sort-vs-sort_by.rb b/code/enumerable/sort-vs-sort_by.rb index 61032448..f1c1936c 100644 --- a/code/enumerable/sort-vs-sort_by.rb +++ b/code/enumerable/sort-vs-sort_by.rb @@ -5,11 +5,12 @@ User.new(sprintf "%010d", rand(1_000_000_000)) end -def fastest +# The two sort_by forms are close: Symbol#to_proc wins on plain CRuby, the block wins with YJIT and on JRuby. +def faster ARRAY.sort_by(&:name) end -def faster +def fast ARRAY.sort_by { |element| element.name } end @@ -18,9 +19,8 @@ def slow end Benchmark.ips do |x| - x.report('Enumerable#sort_by (Symbol#to_proc)') { fastest } - x.report('Enumerable#sort_by') { faster } + x.report('Enumerable#sort_by (Symbol#to_proc)') { faster } + x.report('Enumerable#sort_by') { fast } x.report('Enumerable#sort') { slow } - x.compare! end diff --git a/code/general/attr-accessor-vs-getter-and-setter.rb b/code/general/attr-accessor-vs-getter-and-setter.rb index 6179c905..210a34d6 100644 --- a/code/general/attr-accessor-vs-getter-and-setter.rb +++ b/code/general/attr-accessor-vs-getter-and-setter.rb @@ -10,7 +10,12 @@ def last_name def last_name=(value) @last_name = value end +end +def fast + user = User.new + user.first_name = 'John' + user.first_name end def slow @@ -19,12 +24,6 @@ def slow user.last_name end -def fast - user = User.new - user.first_name = 'John' - user.first_name -end - Benchmark.ips do |x| x.report('attr_accessor') { fast } x.report('getter_and_setter') { slow } diff --git a/code/general/begin-rescue-vs-respond-to.rb b/code/general/begin-rescue-vs-respond-to.rb index 35369392..7751b656 100644 --- a/code/general/begin-rescue-vs-respond-to.rb +++ b/code/general/begin-rescue-vs-respond-to.rb @@ -1,17 +1,17 @@ require 'benchmark/ips' -def slow - begin +def fast + if respond_to?(:writing) writing - rescue + else 'fast ruby' end end -def fast - if respond_to?(:writing) +def slow + begin writing - else + rescue 'fast ruby' end end diff --git a/code/general/define_method-vs-module-eval.rb b/code/general/define_method-vs-module-eval.rb index 0a229e8a..925849b9 100644 --- a/code/general/define_method-vs-module-eval.rb +++ b/code/general/define_method-vs-module-eval.rb @@ -36,7 +36,6 @@ def slow ModuleEvalWithString.def_methods(method_names(10)) end - Benchmark.ips do |x| x.report("define_method") { fast } x.report("module_eval with string") { slow } diff --git a/code/general/format-vs-round-and-to-s.rb b/code/general/format-vs-round-and-to-s.rb index 2556d96c..add92f42 100644 --- a/code/general/format-vs-round-and-to-s.rb +++ b/code/general/format-vs-round-and-to-s.rb @@ -2,11 +2,11 @@ NUM = 1.12678.freeze -def fast +def faster NUM.round(2).to_s end -def avg +def fast format('%.2f', NUM) end @@ -15,8 +15,8 @@ def slow end Benchmark.ips do |x| - x.report('Float#round') { fast } - x.report('Kernel#format') { avg } + x.report('Float#round') { faster } + x.report('Kernel#format') { fast } x.report('String#%') { slow } x.compare! end diff --git a/code/general/inheritance-check.rb b/code/general/inheritance-check.rb index 2c272212..5a4b2b57 100644 --- a/code/general/inheritance-check.rb +++ b/code/general/inheritance-check.rb @@ -2,9 +2,9 @@ # You may ask: 'Is there a project that still using `ancestors.include?`?' # By quick searching, I found the following popular repositories are still using it: -# - rake +# - rake # - https://github.com/ruby/rake/blob/7d0c08fe4e97083a92d2c8fc740cb421fd062117/lib/rake/task_manager.rb#L28 -# - warden +# - warden # - https://github.com/hassox/warden/blob/090ed153dbd2f5bf4a1ca672b3018877e21223a4/lib/warden/strategies.rb#L16 # - metasploit-framework # - https://github.com/rapid7/metasploit-framework/blob/cac890a797d0d770260074dfe703eb5cfb63bd46/lib/msf/core/payload_set.rb#L239 @@ -12,6 +12,8 @@ # - hanami # - https://github.com/hanami/hanami/blob/506a35e5262939eb4dce9195ade3268e19928d00/lib/hanami/components/routes_inspector.rb#L54 # - https://github.com/hanami/hanami/blob/aec069b602c772e279aa0a7f48d1a04d01756ee3/lib/hanami/configuration.rb#L114 + +# Sanity check that both ways give the same answer, so the reports compare equal work. raise unless Object.ancestors.include?(Kernel) raise unless (Object <= Kernel) diff --git a/code/hash/fetch-vs-fetch-with-block.rb b/code/hash/fetch-vs-fetch-with-block.rb index c544d2b5..afabfe38 100644 --- a/code/hash/fetch-vs-fetch-with-block.rb +++ b/code/hash/fetch-vs-fetch-with-block.rb @@ -7,11 +7,11 @@ # a string argument is built on every call, even when the key exists. # The block builds it only when the key is missing, # and the constant is built once, so those two are close. -def fastest +def faster HASH.fetch(:writing, DEFAULT) end -def faster +def fast HASH.fetch(:writing) { "fast ruby" } end @@ -20,8 +20,8 @@ def slow end Benchmark.ips do |x| - x.report("Hash#fetch + const") { fastest } - x.report("Hash#fetch + block") { faster } + x.report("Hash#fetch + const") { faster } + x.report("Hash#fetch + block") { fast } x.report("Hash#fetch + arg") { slow } x.compare! end diff --git a/code/hash/keys-each-vs-each_key.rb b/code/hash/keys-each-vs-each_key.rb index 24ecf5fc..98a4fbb2 100644 --- a/code/hash/keys-each-vs-each_key.rb +++ b/code/hash/keys-each-vs-each_key.rb @@ -39,14 +39,14 @@ } -def slow - HASH.keys.each(&:to_sym) -end - def fast HASH.each_key(&:to_sym) end +def slow + HASH.keys.each(&:to_sym) +end + Benchmark.ips do |x| x.report('Hash#each_key') { fast } x.report('Hash#keys.each') { slow } diff --git a/code/hash/merge-bang-vs-[]=.rb b/code/hash/merge-bang-vs-[]=.rb index 11884ceb..67d3df44 100644 --- a/code/hash/merge-bang-vs-[]=.rb +++ b/code/hash/merge-bang-vs-[]=.rb @@ -2,15 +2,15 @@ ENUM = (1..100) -def slow +def fast ENUM.each_with_object({}) do |e, h| - h.merge!(e => e) + h[e] = e end end -def fast +def slow ENUM.each_with_object({}) do |e, h| - h[e] = e + h.merge!(e => e) end end diff --git a/code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb b/code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb index b4fd86b5..29b45036 100644 --- a/code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb +++ b/code/hash/merge-bang-vs-merge-vs-dup-merge-bang.rb @@ -15,7 +15,7 @@ def slow end end -def slow_dup +def slower ENUM.inject([]) do |accumulator, element| accumulator << ORIGINAL_HASH.dup.merge!(bar: element) end @@ -24,6 +24,6 @@ def slow_dup Benchmark.ips do |x| x.report("{}#merge!(Hash) do end") { fast } x.report("Hash#merge({})") { slow } - x.report("Hash#dup#merge!({})") { slow_dup } + x.report("Hash#dup#merge!({})") { slower } x.compare! end diff --git a/code/hash/merge-vs-merge-bang.rb b/code/hash/merge-vs-merge-bang.rb index e56f85d1..99602b78 100644 --- a/code/hash/merge-vs-merge-bang.rb +++ b/code/hash/merge-vs-merge-bang.rb @@ -2,15 +2,15 @@ ENUM = (1..100) -def slow +def fast ENUM.inject({}) do |h, e| - h.merge(e => e) + h.merge!(e => e) end end -def fast +def slow ENUM.inject({}) do |h, e| - h.merge!(e => e) + h.merge(e => e) end end diff --git a/code/method/call-vs-send-vs-method_missing.rb b/code/method/call-vs-send-vs-method_missing.rb index 2141cd0c..b27c3d18 100644 --- a/code/method/call-vs-send-vs-method_missing.rb +++ b/code/method/call-vs-send-vs-method_missing.rb @@ -9,7 +9,7 @@ def method_missing(_method,*args) end end -def fastest +def fast method = MethodCall.new method.method end @@ -19,14 +19,14 @@ def slow method.send(:method) end -def slowest +def slower method = MethodCall.new method.not_exist end Benchmark.ips do |x| - x.report("call") { fastest } + x.report("call") { fast } x.report("send") { slow } - x.report("method_missing") { slowest } + x.report("method_missing") { slower } x.compare! end diff --git a/code/proc-and-block/block-vs-to_proc.rb b/code/proc-and-block/block-vs-to_proc.rb index 70d68e7f..531fe313 100644 --- a/code/proc-and-block/block-vs-to_proc.rb +++ b/code/proc-and-block/block-vs-to_proc.rb @@ -2,14 +2,14 @@ RANGE = (1..100) -def slow - RANGE.map { |i| i.to_s } -end - def fast RANGE.map(&:to_s) end +def slow + RANGE.map { |i| i.to_s } +end + Benchmark.ips do |x| x.report('Symbol#to_proc') { fast } x.report('Block') { slow } diff --git a/code/proc-and-block/proc-call-vs-yield.rb b/code/proc-and-block/proc-call-vs-yield.rb index f595f5ca..4a763969 100644 --- a/code/proc-and-block/proc-call-vs-yield.rb +++ b/code/proc-and-block/proc-call-vs-yield.rb @@ -1,25 +1,20 @@ require 'benchmark/ips' -def slow(&block) - block.call -end - -def slow2(&block) +def fast yield end -def slow3(&block) - +def slow(&block) + yield end -def fast - yield +def slower(&block) + block.call end Benchmark.ips do |x| - x.report('yield') { fast { 1 + 1 } } - x.report('block.call') { slow { 1 + 1 } } - x.report('block + yield') { slow2 { 1 + 1 } } - x.report('unused block') { slow3 { 1 + 1 } } + x.report('yield') { fast { 1 + 1 } } + x.report('block + yield') { slow { 1 + 1 } } + x.report('block.call') { slower { 1 + 1 } } x.compare! end diff --git a/code/range/cover-vs-include.rb b/code/range/cover-vs-include.rb index bf144cf9..b22aae53 100644 --- a/code/range/cover-vs-include.rb +++ b/code/range/cover-vs-include.rb @@ -23,6 +23,7 @@ def slow (BEGIN_OF_JULY..END_OF_JULY).include? DAY_IN_JULY end +# Range#member? is an alias of Range#include?, so these two should tie. def slower (BEGIN_OF_JULY..END_OF_JULY).member? DAY_IN_JULY end diff --git a/code/string/===-vs-=~-vs-match.rb b/code/string/===-vs-=~-vs-match.rb index 6ac401bd..ec4f7bdc 100644 --- a/code/string/===-vs-=~-vs-match.rb +++ b/code/string/===-vs-=~-vs-match.rb @@ -1,22 +1,23 @@ require "benchmark/ips" +# Regexp#match? and String#match? tie on almost every Ruby; either one could come first. def fastest /boo/.match?('foo'.freeze) end -def fast +def faster "foo".freeze.match?(/boo/) end -def slow +def fast "foo".freeze =~ /boo/ end -def slower - /boo/ === "foo".freeze +def slow + /boo/ === "foo".freeze end -def even_slower +def slower /boo/.match('foo'.freeze) end @@ -26,10 +27,10 @@ def slowest Benchmark.ips do |x| x.report("Regexp#match?") { fastest } if RUBY_VERSION >= "2.4.0".freeze - x.report("String#match?") { fast } if RUBY_VERSION >= "2.4.0".freeze - x.report("String#=~") { slow } - x.report("Regexp#===") { slower } - x.report("Regexp#match") { even_slower } + x.report("String#match?") { faster } if RUBY_VERSION >= "2.4.0".freeze + x.report("String#=~") { fast } + x.report("Regexp#===") { slow } + x.report("Regexp#match") { slower } x.report("String#match") { slowest } x.compare! end diff --git a/code/string/casecmp-vs-downcase-==.rb b/code/string/casecmp-vs-downcase-==.rb index 781fa426..26d5a2eb 100644 --- a/code/string/casecmp-vs-downcase-==.rb +++ b/code/string/casecmp-vs-downcase-==.rb @@ -2,21 +2,21 @@ SLUG = 'ABCD' -def slowest - SLUG.casecmp?('abcd') +def fast + SLUG.casecmp('abcd') == 0 end def slow SLUG.downcase == 'abcd' end -def fast - SLUG.casecmp('abcd') == 0 +def slower + SLUG.casecmp?('abcd') end Benchmark.ips do |x| x.report('String#casecmp') { fast } x.report('String#downcase + ==') { slow } - x.report("String#casecmp?") { slowest } if RUBY_VERSION >= "2.4.0".freeze + x.report("String#casecmp?") { slower } if RUBY_VERSION >= "2.4.0".freeze x.compare! end diff --git a/code/string/concatenation.rb b/code/string/concatenation.rb index e0190a07..9d18a161 100644 --- a/code/string/concatenation.rb +++ b/code/string/concatenation.rb @@ -4,12 +4,12 @@ # 1 object: the result. # Both of these are built from literals when the file is parsed, so they are close; which one wins varies by Ruby version. -def fastest +def faster "#{'foo'}#{'bar'}" end # 1 object: the result. -def faster +def fast 'foo' 'bar' end @@ -29,8 +29,8 @@ def slowest end Benchmark.ips do |x| - x.report('"#{\'foo\'}#{\'bar\'}"') { fastest } - x.report('"foo" "bar"') { faster } + x.report('"#{\'foo\'}#{\'bar\'}"') { faster } + x.report('"foo" "bar"') { fast } x.report('String#append') { slow } x.report('String#concat') { slower } x.report('String#+') { slowest } diff --git a/code/string/end-string-checking-match-vs-end_with.rb b/code/string/end-string-checking-match-vs-end_with.rb index fffdf827..f6d6f090 100644 --- a/code/string/end-string-checking-match-vs-end_with.rb +++ b/code/string/end-string-checking-match-vs-end_with.rb @@ -2,16 +2,16 @@ SLUG = "some_kind_of_root_url" -def slower - SLUG =~ /_(path|url)$/ +def fast + SLUG.end_with?('_path', '_url') end def slow SLUG.match?(/_(path|url)$/) end -def fast - SLUG.end_with?('_path', '_url') +def slower + SLUG =~ /_(path|url)$/ end Benchmark.ips do |x| diff --git a/code/string/gsub-vs-sub.rb b/code/string/gsub-vs-sub.rb index 8bc4d97f..d1d853f2 100644 --- a/code/string/gsub-vs-sub.rb +++ b/code/string/gsub-vs-sub.rb @@ -2,22 +2,22 @@ URL = 'http://www.thelongestlistofthelongeststuffatthelongestdomainnameatlonglast.com/wearejustdoingthistobestupidnowsincethiscangoonforeverandeverandeverbutitstilllookskindaneatinthebrowsereventhoughitsabigwasteoftimeandenergyandhasnorealpointbutwehadtodoitanyways.html' -def slow - URL.gsub('http://', 'https://') +def faster + str = URL.dup + str['http://'] = 'https://' + str end def fast URL.sub('http://', 'https://') end -def fastest - str = URL.dup - str['http://'] = 'https://' - str +def slow + URL.gsub('http://', 'https://') end Benchmark.ips do |x| - x.report('String#dup["string"]=') { fastest } + x.report('String#dup["string"]=') { faster } x.report('String#sub') { fast } x.report('String#gsub') { slow } x.compare! diff --git a/code/string/gsub-vs-tr.rb b/code/string/gsub-vs-tr.rb index 9c2f306b..aaa97ee7 100644 --- a/code/string/gsub-vs-tr.rb +++ b/code/string/gsub-vs-tr.rb @@ -2,14 +2,14 @@ SLUG = 'writing-fast-ruby' -def slow - SLUG.gsub('-', ' ') -end - def fast SLUG.tr('-', ' ') end +def slow + SLUG.gsub('-', ' ') +end + Benchmark.ips do |x| x.report('String#tr') { fast } x.report('String#gsub') { slow } diff --git a/code/string/remove-extra-spaces-or-other-chars.rb b/code/string/remove-extra-spaces-or-other-chars.rb index c28ecefc..55ebee5c 100644 --- a/code/string/remove-extra-spaces-or-other-chars.rb +++ b/code/string/remove-extra-spaces-or-other-chars.rb @@ -10,14 +10,14 @@ raise unless PASSAGE.gsub(/ +/, " ") == PASSAGE.squeeze(" ") -def slow - PASSAGE.gsub(/ +/, " ") -end - def fast PASSAGE.squeeze(" ") end +def slow + PASSAGE.gsub(/ +/, " ") +end + Benchmark.ips do |x| x.report('String#squeeze') { fast } x.report('String#gsub/regex+/') { slow } diff --git a/code/string/start-string-checking-match-vs-start_with.rb b/code/string/start-string-checking-match-vs-start_with.rb index e5a9ba6e..5b4bddd3 100644 --- a/code/string/start-string-checking-match-vs-start_with.rb +++ b/code/string/start-string-checking-match-vs-start_with.rb @@ -2,18 +2,18 @@ SLUG = 'test_some_kind_of_long_file_name.rb' -def slower - SLUG =~ /^test_/ +# String#match? wins on Ruby 3.4 (with or without YJIT). +# start_with? wins on the other Rubies CI runs. +def fast + SLUG.start_with?('test_') end def slow SLUG.match?(/^test_/) end -# String#match? wins on Ruby 3.4 (with or without YJIT). -# start_with? wins on the other Rubies CI runs. -def fast - SLUG.start_with?('test_') +def slower + SLUG =~ /^test_/ end Benchmark.ips do |x| diff --git a/code/string/sub!-vs-gsub!-vs-[]=.rb b/code/string/sub!-vs-gsub!-vs-[]=.rb index bb91e9d3..98ce1eff 100644 --- a/code/string/sub!-vs-gsub!-vs-[]=.rb +++ b/code/string/sub!-vs-gsub!-vs-[]=.rb @@ -2,43 +2,42 @@ URL = "http://www.thelongestlistofthelongeststuffatthelongestdomainnameatlonglast.com/wearejustdoingthistobestupidnowsincethiscangoonforeverandeverandeverbutitstilllookskindaneatinthebrowsereventhoughitsabigwasteoftimeandenergyandhasnorealpointbutwehadtodoitanyways.html" -def fast +def fastest s = URL.dup s["http://"] = "" end -def slow_1 +def faster s = URL.dup - s.sub! "http://", "" + s[%r{http://}] = "" end -def slow_2 +def fast s = URL.dup - s.gsub! "http://", "" + s.sub! "http://", "" end -def slow_3 +def slow s = URL.dup - s[%r{http://}] = "" + s.sub! %r{http://}, "" end -def slow_4 +def slower s = URL.dup - s.sub! %r{http://}, "" + s.gsub! "http://", "" end -def slow_5 +def slowest s = URL.dup s.gsub! %r{http://}, "" end - Benchmark.ips do |x| - x.report("String#['string']=") { fast } - x.report("String#sub!'string'") { slow_1 } - x.report("String#gsub!'string'") { slow_2 } - x.report("String#[/regexp/]=") { slow_3 } - x.report("String#sub!/regexp/") { slow_4 } - x.report("String#gsub!/regexp/") { slow_5 } + x.report("String#['string']=") { fastest } + x.report("String#[/regexp/]=") { faster } + x.report("String#sub!'string'") { fast } + x.report("String#sub!/regexp/") { slow } + x.report("String#gsub!'string'") { slower } + x.report("String#gsub!/regexp/") { slowest } x.compare! end diff --git a/code/string/sub-vs-chomp-vs-delete_suffix.rb b/code/string/sub-vs-chomp-vs-delete_suffix.rb index a5ae925f..84eb4a9d 100644 --- a/code/string/sub-vs-chomp-vs-delete_suffix.rb +++ b/code/string/sub-vs-chomp-vs-delete_suffix.rb @@ -2,16 +2,16 @@ SLUG = 'YourSubclassType' -def slow - SLUG.sub(/Type\z/, '') +def faster + SLUG.delete_suffix('Type') end def fast SLUG.chomp('Type') end -def faster - SLUG.delete_suffix('Type') +def slow + SLUG.sub(/Type\z/, '') end Benchmark.ips do |x| From c50ae44bc432996d28ebd36ef21350f802f370a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Fri, 25 Sep 2026 08:20:50 -0600 Subject: [PATCH 4/4] Step 4: Rerun the proc-call-vs-yield README sample Step 3 dropped its "unused block" report, but the README still showed it ranked first, which would give readers the wrong ranking. The new sample is from ruby_4.0: yield, then block + yield (1.23x slower), then block.call (1.41x slower), the same order as 22 of 26 jobs in CI run 35927028164. --- README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 8e0cd5b0..a14cee73 100644 --- a/README.md +++ b/README.md @@ -1061,23 +1061,21 @@ Comparison: ``` $ ruby -v code/proc-and-block/proc-call-vs-yield.rb -ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] +ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux] 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 + yield 2.156M i/100ms + block + yield 1.605M i/100ms + block.call 1.609M 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 + yield 20.615M (±10.0%) i/s (48.51 ns/i) - 103.495M in 5.020450s + block + yield 16.824M (± 9.2%) i/s (59.44 ns/i) - 85.066M in 5.056288s + block.call 14.608M (±14.3%) i/s (68.46 ns/i) - 73.995M in 5.065432s 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 + yield: 20614679.3 i/s +block + yield: 16823796.9 i/s - 1.23x slower + block.call: 14607819.3 i/s - 1.41x slower + ``` ### String