From 1f7d27e89d655540c547c474bec19bd27c040202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 29 Sep 2026 00:24:44 -0600 Subject: [PATCH 1/2] Step 1: Add String#match? vs include? vs =~ for a fixed substring (#139) From #139 by @lvl0nax, which added String#include? to the Regexp benchmark, ===-vs-=~-vs-match.rb. include? does not match a Regexp, it looks for a fixed substring, so it gets its own file, include-vs-match.rb, against the Regexp ways to find the same substring. The PR found include? about 2x faster than =~ on Ruby 2.4.2, and it still beats =~ everywhere. String#match?, added in Ruby 2.4, is as fast or faster on current CRuby, so it is named the winner: faster match?, fast include?, slow =~. On Ruby 3.4 match? is 1.39x ahead of include?, on 4.0 1.17x; include? wins on 2.1, which has no match? (that report is guarded), and on JRuby. README: - The ===-vs-=~-vs-match entry gets back its Ruby 2.2.2 sample from the git history, and keeps #139's Ruby 2.4.2 run (the Regexp file plus include?) with a note, before the 4.0.0 sample main had. - A new entry for include-vs-match.rb, with runs on Ruby 3.4 and 4.0.7. Co-authored-by: Dmitrii Gautsel <1892604+lvl0nax@users.noreply.github.com> --- README.md | 83 +++++++++++++++++++++++++++++++++ code/string/include-vs-match.rb | 22 +++++++++ 2 files changed, 105 insertions(+) create mode 100644 code/string/include-vs-match.rb diff --git a/README.md b/README.md index 69c4dca..0a17558 100644 --- a/README.md +++ b/README.md @@ -1704,6 +1704,49 @@ improvement. > :warning:
> `Regexp#===` is also faster than `String#match` but you need to switch the order of arguments. +``` +$ ruby -v code/string/===-vs-=~-vs-match.rb +ruby 2.2.2p95 (2015-04-13 revision 50295) [x86_64-darwin14] + +Calculating ------------------------------------- + String#=~ 98.184k i/100ms + Regexp#=== 92.382k i/100ms + String#match 83.601k i/100ms +------------------------------------------------- + String#=~ 2.442M (± 7.6%) i/s - 12.175M + Regexp#=== 2.259M (± 7.9%) i/s - 11.271M + String#match 1.840M (± 7.3%) i/s - 9.196M + +Comparison: + String#=~: 2442335.1 i/s + Regexp#===: 2259277.3 i/s - 1.08x slower + String#match: 1839815.4 i/s - 1.33x slower +``` + +The next run is from #139, which added `String#include?` to this file; it now has its own entry below. + +``` +$ ruby -v code/string/===-vs-=~-vs-match.rb +ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16] + +Warming up -------------------------------------- + String#include? 238.465k i/100ms + String#=~ 161.826k i/100ms + Regexp#=== 147.986k i/100ms + String#match 136.749k i/100ms +Calculating ------------------------------------- + String#include? 5.961M (± 8.5%) i/s - 29.808M in 5.035615s + String#=~ 2.843M (± 8.1%) i/s - 14.241M in 5.040590s + Regexp#=== 2.689M (± 7.3%) i/s - 13.467M in 5.032779s + String#match 2.371M (± 5.2%) i/s - 11.897M in 5.031192s + +Comparison: + String#include?: 5960785.4 i/s + String#=~: 2843384.2 i/s - 2.10x slower + Regexp#===: 2689290.6 i/s - 2.22x slower + String#match: 2370791.0 i/s - 2.51x slower +``` + ``` $ ruby -v code/string/===-vs-=~-vs-match.rb ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin24] @@ -1734,6 +1777,46 @@ Comparison: See [#59](https://github.com/fastruby/fast-ruby/pull/59) and [#62](https://github.com/fastruby/fast-ruby/pull/62) for discussions. +##### `String#match?` vs `String#include?` vs `String#=~` for a fixed substring [code](code/string/include-vs-match.rb) + +For a fixed substring, `String#include?` beats a Regexp with `=~`, and `String#match?` (Ruby 2.4+) is as fast or faster. + +``` +$ ruby -v code/string/include-vs-match.rb +ruby 3.4.10 (2026-06-30 revision 2b0b7728dc) +PRISM [aarch64-linux] +Warming up -------------------------------------- + String#match? 1.325M i/100ms + String#include? 995.530k i/100ms + String#=~ 853.136k i/100ms +Calculating ------------------------------------- + String#match? 13.827M (± 3.2%) i/s (72.32 ns/i) - 70.248M in 5.080506s + String#include? 9.974M (± 1.5%) i/s (100.26 ns/i) - 50.772M in 5.090292s + String#=~ 8.489M (± 4.8%) i/s (117.81 ns/i) - 42.657M in 5.025211s + +Comparison: + String#match?: 13826876.4 i/s +String#include?: 9974287.2 i/s - 1.39x slower + String#=~: 8488559.2 i/s - 1.63x slower +``` + +``` +$ ruby -v code/string/include-vs-match.rb +ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux] +Warming up -------------------------------------- + String#match? 1.517M i/100ms + String#include? 1.283M i/100ms + String#=~ 1.004M i/100ms +Calculating ------------------------------------- + String#match? 14.967M (± 3.4%) i/s (66.81 ns/i) - 75.870M in 5.069146s + String#include? 12.845M (± 0.5%) i/s (77.85 ns/i) - 65.441M in 5.094510s + String#=~ 10.039M (± 1.0%) i/s (99.61 ns/i) - 50.218M in 5.002306s + +Comparison: + String#match?: 14967056.6 i/s +String#include?: 12845487.5 i/s - 1.17x slower + String#=~: 10038889.7 i/s - 1.49x slower +``` + ##### `String#gsub` vs `String#sub` vs `String#[]=` [code](code/string/gsub-vs-sub.rb) ``` diff --git a/code/string/include-vs-match.rb b/code/string/include-vs-match.rb new file mode 100644 index 0000000..e417626 --- /dev/null +++ b/code/string/include-vs-match.rb @@ -0,0 +1,22 @@ +require 'benchmark/ips' + +# For a fixed substring, String#include? beats a Regexp with =~. +# String#match? (Ruby 2.4+) is as fast as include? or faster on current CRuby. +def faster + 'foo'.freeze.match?(/boo/) +end + +def fast + 'foo'.freeze.include?('boo') +end + +def slow + 'foo'.freeze =~ /boo/ +end + +Benchmark.ips do |x| + x.report('String#match?') { faster } if RUBY_VERSION >= '2.4.0' + x.report('String#include?') { fast } + x.report('String#=~') { slow } + x.compare! +end From be66ea1479cc1958bbd10a2fbdf02761bca25d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20V=C3=A1squez?= Date: Tue, 29 Sep 2026 07:59:58 -0600 Subject: [PATCH 2/2] Step 2: Add begin...end until, until false and an endless range to loop vs while true (#228, #143) From #143 by @styd (begin...end until) and #228 by @ydakuka (until false and an endless range), which both extend loop-vs-while-true.rb, so they land together as five reports. Changes to the PRs: - Kept the file name loop-vs-while-true.rb instead of #228's new one, so its results keep their history, and the existing labels. - Rank names: fastest begin...end until (first on Ruby 2.1 and 3.4), faster while true, fast until false, slow loop, slower endless range. The three loops that test a condition tie on Ruby 4.0; a comment says so. - (0..) is Ruby 2.6+ syntax, which older Rubies cannot even parse, so that method is defined from a string with eval and its report only runs on 2.6+. (0..Float::INFINITY) would parse, but it is 2x to 2.6x slower than (0..), so it would not measure the same thing. - Dropped the editor swap file .README.md.swp that #143 committed. The README entry names all five and keeps every run: 2.2.3 from the git history, 2.5.0 from #143, 3.3.0dev from #228, new 3.4 and 4.0.7 runs, and 4.0.0 as on main. Co-authored-by: Adrian Setyadi <8341867+styd@users.noreply.github.com> Co-authored-by: Yauheni Dakuka <10832262+ydakuka@users.noreply.github.com> --- README.md | 109 ++++++++++++++++++++++++++++- code/general/loop-vs-while-true.rb | 38 ++++++++-- 2 files changed, 142 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0a17558..7fa8007 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,90 @@ Custom exception: Kernel#raise: 2598894.6 i/s Custom exception: E2MM#Raise: 88322.1 i/s - 29.43x slower ``` -##### `loop` vs `while true` [code](code/general/loop-vs-while-true.rb) +##### `begin...end until` vs `while true` vs `until false` vs `loop` vs infinite range [code](code/general/loop-vs-while-true.rb) + +The first run only had `while true` and `loop`. +The second is from #143, which added `begin...end until`, and the third from #228, which added `until false` and the infinite range. + +``` +$ ruby -v code/general/loop-vs-while-true.rb +ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux] + +Calculating ------------------------------------- + While Loop 1.000 i/100ms + Kernel loop 1.000 i/100ms +------------------------------------------------- + While Loop 0.536 (± 0.0%) i/s - 3.000 in 5.593042s + Kernel loop 0.223 (± 0.0%) i/s - 2.000 in 8.982355s + +Comparison: + While Loop: 0.5 i/s + Kernel loop: 0.2 i/s - 2.41x slower +``` + +``` +$ ruby -v code/general/loop-vs-while-true.rb +ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux] +Warming up -------------------------------------- + Begin Until 1.000 i/100ms + While Loop 1.000 i/100ms + Kernel loop 1.000 i/100ms +Calculating ------------------------------------- + Begin Until 0.211 (± 0.0%) i/s - 2.000 in 9.469782s + While Loop 0.192 (± 0.0%) i/s - 1.000 in 5.212417s + Kernel loop 0.072 (± 0.0%) i/s - 1.000 in 13.921672s + +Comparison: + Begin Until: 0.2 i/s + While Loop: 0.2 i/s - 1.10x slower + Kernel loop: 0.1 i/s - 2.94x slower +``` + +``` +$ ruby -v code/general/loop-vs-while-true-vs-until-false-vs-infinite-range.rb +ruby 3.3.0dev (2023-11-12 master 60e19a0b5f) [x86_64-linux] + +Warming up -------------------------------------- + While loop 1.000 i/100ms + Until loop 1.000 i/100ms + Kernel loop 1.000 i/100ms + Infinite range 1.000 i/100ms +Calculating ------------------------------------- + While loop 0.600 (± 0.0%) i/s - 3.000 in 5.002930s + Until loop 0.594 (± 0.0%) i/s - 3.000 in 5.054004s + Kernel loop 0.258 (± 0.0%) i/s - 2.000 in 7.754873s + Infinite range 0.207 (± 0.0%) i/s - 2.000 in 9.664227s + +Comparison: + While loop: 0.6 i/s + Until loop: 0.6 i/s - 1.01x slower + Kernel loop: 0.3 i/s - 2.33x slower + Infinite range: 0.2 i/s - 2.90x slower +``` + +``` +$ ruby -v code/general/loop-vs-while-true.rb +ruby 3.4.10 (2026-06-30 revision 2b0b7728dc) +PRISM [aarch64-linux] +Warming up -------------------------------------- + Begin Until 1.000 i/100ms + While Loop 1.000 i/100ms + Until loop 1.000 i/100ms + Kernel loop 1.000 i/100ms + Infinite range 1.000 i/100ms +Calculating ------------------------------------- + Begin Until 0.715 (± 0.0%) i/s (1.40 s/i) - 4.000 in 5.593345s + While Loop 0.713 (± 0.0%) i/s (1.40 s/i) - 4.000 in 5.607034s + Until loop 0.710 (± 0.0%) i/s (1.41 s/i) - 4.000 in 5.634041s + Kernel loop 0.387 (± 0.0%) i/s (2.59 s/i) - 2.000 in 5.172241s + Infinite range 0.399 (± 0.0%) i/s (2.50 s/i) - 2.000 in 5.008409s + +Comparison: + Begin Until: 0.7 i/s + While Loop: 0.7 i/s - 1.00x slower + Until loop: 0.7 i/s - 1.01x slower +Infinite range: 0.4 i/s - 1.79x slower + Kernel loop: 0.4 i/s - 1.85x slower +``` ``` $ ruby -v code/general/loop-vs-while-true.rb @@ -235,6 +318,30 @@ Comparison: Kernel loop: 0.5 i/s - 2.52x slower ``` +``` +$ ruby -v code/general/loop-vs-while-true.rb +ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux] +Warming up -------------------------------------- + Begin Until 1.000 i/100ms + While Loop 1.000 i/100ms + Until loop 1.000 i/100ms + Kernel loop 1.000 i/100ms + Infinite range 1.000 i/100ms +Calculating ------------------------------------- + Begin Until 0.666 (± 0.0%) i/s (1.50 s/i) - 4.000 in 6.006927s + While Loop 0.687 (± 0.0%) i/s (1.46 s/i) - 4.000 in 5.826603s + Until loop 0.686 (± 0.0%) i/s (1.46 s/i) - 4.000 in 5.828591s + Kernel loop 0.520 (± 0.0%) i/s (1.92 s/i) - 3.000 in 5.767842s + Infinite range 0.386 (± 0.0%) i/s (2.59 s/i) - 2.000 in 5.184388s + +Comparison: + While Loop: 0.7 i/s + Until loop: 0.7 i/s - 1.00x slower + Begin Until: 0.7 i/s - 1.03x slower + Kernel loop: 0.5 i/s - 1.32x slower +Infinite range: 0.4 i/s - 1.78x slower +``` + ##### `ancestors.include?` vs `<=` [code](code/general/inheritance-check.rb) ``` diff --git a/code/general/loop-vs-while-true.rb b/code/general/loop-vs-while-true.rb index 33ae139..2c65971 100644 --- a/code/general/loop-vs-while-true.rb +++ b/code/general/loop-vs-while-true.rb @@ -1,8 +1,16 @@ -require "benchmark/ips" +require 'benchmark/ips' NUMBER = 100_000_000 -def fast +# The three loops that test a condition tie on Ruby 4.0. +def fastest + index = 0 + begin + index += 1 + end until index > NUMBER +end + +def faster index = 0 while true break if index > NUMBER @@ -10,6 +18,14 @@ def fast end end +def fast + index = 0 + until false + break if index > NUMBER + index += 1 + end +end + def slow index = 0 loop do @@ -18,8 +34,22 @@ def slow end end +# Endless ranges are Ruby 2.6+ syntax, so the method is defined from a string, which older Rubies never parse. +if RUBY_VERSION >= '2.6.0' + eval <<-RUBY + def slower + (0..).each do |index| + break if index > NUMBER + end + end + RUBY +end + Benchmark.ips do |x| - x.report("While Loop") { fast } - x.report("Kernel loop") { slow } + x.report('Begin Until') { fastest } + x.report('While Loop') { faster } + x.report('Until loop') { fast } + x.report('Kernel loop') { slow } + x.report('Infinite range') { slower } if RUBY_VERSION >= '2.6.0' x.compare! end