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..e4ee7dbb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,37 @@ 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. +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 faster + ARRAY.length +end + +# Array#size is an alias of Array#length, so these two should tie. +def fast + ARRAY.size +end + +def slow + ARRAY.count +end + +Benchmark.ips do |x| + x.report("Array#length") { faster } + x.report("Array#size") { fast } + x.report("Array#count") { slow } + x.compare! +end +``` Run your result: diff --git a/README.md b/README.md index cecf3ce9..a14cee73 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 @@ -1058,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 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..47e3b810 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 faster + ARRAY.length +end + +# Array#size is an alias of Array#length, so these two should tie. +def fast + 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") { 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 49a639b4..374e1b42 100644 --- a/code/array/shuffle-first-vs-sample.rb +++ b/code/array/shuffle-first-vs-sample.rb @@ -2,16 +2,16 @@ 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#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..e5a6f541 100644 --- a/code/enumerable/each-push-vs-map.rb +++ b/code/enumerable/each-push-vs-map.rb @@ -2,17 +2,17 @@ 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#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..7fd611ba 100644 --- a/code/enumerable/each-vs-for-loop.rb +++ b/code/enumerable/each-vs-for-loop.rb @@ -2,20 +2,20 @@ 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 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/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 25e0e79e..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#map.flatten(1)') { slow_flatten_1 } - x.report('Array#map.flatten') { slow_flatten } - x.report('Array#flat_map') { fast } + 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 2ddf6a86..f1f15ae2 100644 --- a/code/enumerable/reverse-each-vs-reverse_each.rb +++ b/code/enumerable/reverse-each-vs-reverse_each.rb @@ -2,16 +2,16 @@ 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') { 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..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#select.first') { slow } - x.report('Enumerable#detect') { fast } 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 f6c442b3..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,14 +24,8 @@ 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('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..7751b656 100644 --- a/code/general/begin-rescue-vs-respond-to.rb +++ b/code/general/begin-rescue-vs-respond-to.rb @@ -1,23 +1,23 @@ 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 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..925849b9 100644 --- a/code/general/define_method-vs-module-eval.rb +++ b/code/general/define_method-vs-module-eval.rb @@ -36,9 +36,8 @@ def slow ModuleEvalWithString.def_methods(method_names(10)) end - 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/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/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..afabfe38 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 faster + HASH.fetch(:writing, DEFAULT) +end + +def fast + 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") { 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 fd3dd6a6..98a4fbb2 100644 --- a/code/hash/keys-each-vs-each_key.rb +++ b/code/hash/keys-each-vs-each_key.rb @@ -39,16 +39,16 @@ } -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#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..67d3df44 100644 --- a/code/hash/merge-bang-vs-[]=.rb +++ b/code/hash/merge-bang-vs-[]=.rb @@ -2,20 +2,20 @@ 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 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-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 df501d5b..99602b78 100644 --- a/code/hash/merge-vs-merge-bang.rb +++ b/code/hash/merge-vs-merge-bang.rb @@ -2,20 +2,20 @@ 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 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/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 10845e14..531fe313 100644 --- a/code/proc-and-block/block-vs-to_proc.rb +++ b/code/proc-and-block/block-vs-to_proc.rb @@ -2,16 +2,16 @@ 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('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..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('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('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 8d23c1b4..b22aae53 100644 --- a/code/range/cover-vs-include.rb +++ b/code/range/cover-vs-include.rb @@ -5,12 +5,34 @@ 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 + +# 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 + 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/===-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 be46803f..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?") { 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?") { slower } if RUBY_VERSION >= "2.4.0".freeze x.compare! end diff --git a/code/string/concatenation.rb b/code/string/concatenation.rb index e249a307..9d18a161 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 faster + "#{'foo'}#{'bar'}" end -# 2 + 1 = 3 object -def slow_concat - 'foo'.concat 'bar' +# 1 object: the result. +def fast + '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\'}"') { faster } x.report('"foo" "bar"') { fast } - x.report('"#{\'foo\'}#{\'bar\'}"') { fast_interpolation } + 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..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,21 +2,21 @@ 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| - 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..d1d853f2 100644 --- a/code/string/gsub-vs-sub.rb +++ b/code/string/gsub-vs-sub.rb @@ -2,23 +2,23 @@ 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#gsub') { slow } + x.report('String#dup["string"]=') { faster } x.report('String#sub') { fast } - x.report('String#dup["string"]=') { fastest } + 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..aaa97ee7 100644 --- a/code/string/gsub-vs-tr.rb +++ b/code/string/gsub-vs-tr.rb @@ -2,16 +2,16 @@ 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#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..55ebee5c 100644 --- a/code/string/remove-extra-spaces-or-other-chars.rb +++ b/code/string/remove-extra-spaces-or-other-chars.rb @@ -10,16 +10,16 @@ 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#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..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,21 +2,23 @@ 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 -def fast - SLUG.start_with?('test_') +def slower + SLUG =~ /^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-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 6ecfcf9f..84eb4a9d 100644 --- a/code/string/sub-vs-chomp-vs-delete_suffix.rb +++ b/code/string/sub-vs-chomp-vs-delete_suffix.rb @@ -2,21 +2,21 @@ 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| - 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