Skip to content

Commit 86836b0

Browse files
authored
Merge pull request #244 from fastruby/feature/adopt-open-prs
Adopt five open benchmark PRs
2 parents f579b70 + 994cecc commit 86836b0

6 files changed

Lines changed: 352 additions & 0 deletions

‎README.md‎

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,47 @@ using an if statement: 15517955.2 i/s
134134
String#constantize: 10556362.4 i/s - 1.47x slower
135135
```
136136

137+
##### Method vs forwarded method vs delegated method [code](code/general/method-vs-forwarded-method-vs-delegated-method.rb)
138+
139+
```
140+
$ ruby -v code/general/method-vs-forwarded-method-vs-delegated-method.rb
141+
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin19]
142+
Warming up --------------------------------------
143+
method 247.079k i/100ms
144+
forwarded method 183.323k i/100ms
145+
delegated method 136.666k i/100ms
146+
Calculating -------------------------------------
147+
method 2.481M (± 0.5%) i/s - 12.601M in 5.078755s
148+
forwarded method 1.830M (± 0.8%) i/s - 9.166M in 5.009606s
149+
delegated method 1.365M (± 0.6%) i/s - 6.833M in 5.006191s
150+
151+
Comparison:
152+
method: 2481182.0 i/s
153+
forwarded method: 1829835.8 i/s - 1.36x (± 0.00) slower
154+
delegated method: 1365026.7 i/s - 1.82x (± 0.00) slower
155+
```
156+
157+
The run above is from the PR, when the delegated report called `pop(1)`.
158+
The one below measures the current code, where all three push `1`.
159+
160+
```
161+
$ ruby -v code/general/method-vs-forwarded-method-vs-delegated-method.rb
162+
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux]
163+
Warming up --------------------------------------
164+
method 566.282k i/100ms
165+
forwarded method 452.579k i/100ms
166+
delegated method 222.869k i/100ms
167+
Calculating -------------------------------------
168+
method 5.690M (± 1.0%) i/s (175.74 ns/i) - 28.880M in 5.075387s
169+
forwarded method 4.580M (± 3.9%) i/s (218.33 ns/i) - 23.082M in 5.039438s
170+
delegated method 2.229M (± 2.1%) i/s (448.72 ns/i) - 11.143M in 5.000248s
171+
172+
Comparison:
173+
method: 5690282.1 i/s
174+
forwarded method: 4580179.6 i/s - 1.24x slower
175+
delegated method: 2228579.5 i/s - 2.55x slower
176+
```
177+
137178
##### `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)
138179

139180
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.
@@ -495,6 +536,70 @@ Comparison:
495536
Array#sort_by &:-@: 229323.6 i/s - 2.44x slower
496537
```
497538

539+
##### Subset check: `(a1 - a2).empty?` vs alternatives [code](code/array/subset-check.rb)
540+
541+
> To check whether every element of `a1` is also in `a2`, `(a1 - a2).empty?` is
542+
> consistently the fastest across modern Ruby versions for the common case where
543+
> `a1` is actually a subset. <br>
544+
> **Caveat:** the winner is highly data-dependent. `a1.all? { |e| a2.include?(e) }`
545+
> short-circuits on the first miss, so it wins when `a1` is *not* a subset, but it
546+
> is O(n*m) and degrades badly on large true subsets. <br>
547+
> Note also that `Set#subset?` (including the `to_set` conversion) was ~6.8x slower
548+
> on Ruby 3.3/3.4 but only ~1.7x slower on Ruby 4.0, where `Set` became much
549+
> faster. If you already hold `Set`s (or check repeatedly), `Set#subset?` scales
550+
> best.
551+
552+
```
553+
$ ruby -v code/array/subset-check.rb
554+
ruby 3.3.10 (2025-10-23 revision 343ea05002) [arm64-darwin25]
555+
Warming up --------------------------------------
556+
(a1 - a2).empty? 86.499k i/100ms
557+
(a1 & a2) == a1 73.860k i/100ms
558+
(a1 & a2).size == n 74.102k i/100ms
559+
a1.all? { include? } 67.703k i/100ms
560+
a1.to_set.subset? 12.068k i/100ms
561+
Calculating -------------------------------------
562+
(a1 - a2).empty? 849.546k (± 2.7%) i/s (1.18 μs/i) - 4.325M in 5.090893s
563+
(a1 & a2) == a1 746.103k (± 2.0%) i/s (1.34 μs/i) - 3.767M in 5.048711s
564+
(a1 & a2).size == n 780.447k (± 1.9%) i/s (1.28 μs/i) - 3.927M in 5.032254s
565+
a1.all? { include? } 715.301k (± 2.4%) i/s (1.40 μs/i) - 3.588M in 5.016435s
566+
a1.to_set.subset? 124.189k (± 0.8%) i/s (8.05 μs/i) - 627.536k in 5.053052s
567+
568+
Comparison:
569+
(a1 - a2).empty?: 849546.4 i/s
570+
(a1 & a2).size == n: 780446.7 i/s - 1.09x slower
571+
(a1 & a2) == a1: 746103.3 i/s - 1.14x slower
572+
a1.all? { include? }: 715300.6 i/s - 1.19x slower
573+
a1.to_set.subset?: 124189.5 i/s - 6.84x slower
574+
575+
$ ruby -v code/array/subset-check.rb
576+
ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +PRISM [arm64-darwin25]
577+
Comparison:
578+
(a1 - a2).empty?: 807172.5 i/s
579+
(a1 & a2).size == n: 719880.2 i/s - 1.12x slower
580+
a1.all? { include? }: 713213.0 i/s - 1.13x slower
581+
(a1 & a2) == a1: 687629.1 i/s - 1.17x slower
582+
a1.to_set.subset?: 119834.6 i/s - 6.74x slower
583+
584+
$ ruby -v code/array/subset-check.rb
585+
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25]
586+
Comparison:
587+
(a1 - a2).empty?: 784706.1 i/s
588+
a1.all? { include? }: 727520.9 i/s - 1.08x slower
589+
(a1 & a2).size == n: 701969.3 i/s - 1.12x slower
590+
(a1 & a2) == a1: 670252.7 i/s - 1.17x slower
591+
a1.to_set.subset?: 467206.3 i/s - 1.68x slower
592+
593+
$ ruby -v code/array/subset-check.rb
594+
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux]
595+
Comparison:
596+
(a1 - a2).empty?: 547122.9 i/s
597+
a1.all? { include? }: 506861.9 i/s - same-ish: difference falls within error
598+
(a1 & a2).size == n: 427211.2 i/s - 1.28x slower
599+
a1.to_set.subset?: 341619.6 i/s - 1.60x slower
600+
(a1 & a2) == a1: 329130.1 i/s - 1.66x slower
601+
```
602+
498603
### Enumerable
499604

500605
##### `Enumerable#each + push` vs `Enumerable#map` [code](code/enumerable/each-push-vs-map.rb)
@@ -703,6 +808,26 @@ Comparison:
703808
inject block: 39550.0 i/s - 46.54x slower
704809
```
705810

811+
##### `Array#sum` vs `Array#inject(:+)` [code](code/enumerable/inject-symbol-plus-vs-sum.rb)
812+
813+
`Array#sum` needs Ruby 2.4 or newer.
814+
For Floats the two differ: `Array#sum` compensates for rounding errors.
815+
816+
```
817+
$ ruby -v code/enumerable/inject-symbol-plus-vs-sum.rb
818+
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux]
819+
Warming up --------------------------------------
820+
Array#sum 157.012k i/100ms
821+
Array#inject(:+) 100.674k i/100ms
822+
Calculating -------------------------------------
823+
Array#sum 1.527M (± 4.2%) i/s (655.01 ns/i) - 7.694M in 5.039404s
824+
Array#inject(:+) 977.701k (± 9.6%) i/s (1.02 μs/i) - 4.933M in 5.045534s
825+
826+
Comparison:
827+
Array#sum: 1526686.2 i/s
828+
Array#inject(:+): 977701.4 i/s - 1.56x slower
829+
```
830+
706831
### Date
707832

708833
##### `Date.iso8601` vs `Date.parse` [code](code/date/iso8601-vs-parse.rb)
@@ -896,6 +1021,48 @@ Comparison:
8961021
Hash#values.include?: 62052.8 i/s - 1.15x slower
8971022
```
8981023

1024+
##### `Hash#values.compact` instead of `Hash#values.select` or `Hash#select.values` (to get non-nil values) [code](code/hash/select-values-vs-values-select-vs-values-compact.rb)
1025+
1026+
> To collect the non-nil values of a hash, `Hash#select { |_k, v| !v.nil? }.values` allocates an intermediate hash before extracting its values; <br>
1027+
> `Hash#values.select { |v| !v.nil? }` skips the intermediate hash but still runs a block per element; <br>
1028+
> `Hash#values.compact` drops the nils in C without a Ruby-level block, which is fastest.
1029+
1030+
```
1031+
$ ruby -v code/hash/select-values-vs-values-select-vs-values-compact.rb
1032+
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25]
1033+
Warming up --------------------------------------
1034+
Hash#select.values 2.887k i/100ms
1035+
Hash#values.select 3.526k i/100ms
1036+
Hash#values.compact 57.606k i/100ms
1037+
Calculating -------------------------------------
1038+
Hash#select.values 29.102k (± 1.3%) i/s (34.36 μs/i) - 147.237k in 5.060206s
1039+
Hash#values.select 35.490k (± 0.7%) i/s (28.18 μs/i) - 179.826k in 5.067223s
1040+
Hash#values.compact 580.469k (± 4.2%) i/s (1.72 μs/i) - 2.938M in 5.070648s
1041+
1042+
Comparison:
1043+
Hash#values.compact: 580468.7 i/s
1044+
Hash#values.select: 35489.9 i/s - 16.36x slower
1045+
Hash#select.values: 29101.7 i/s - 19.95x slower
1046+
```
1047+
1048+
```
1049+
$ ruby -v code/hash/select-values-vs-values-select-vs-values-compact.rb
1050+
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux]
1051+
Warming up --------------------------------------
1052+
Hash#values.compact 58.469k i/100ms
1053+
Hash#values.select 2.835k i/100ms
1054+
Hash#select.values 2.604k i/100ms
1055+
Calculating -------------------------------------
1056+
Hash#values.compact 598.295k (± 1.7%) i/s (1.67 μs/i) - 3.040M in 5.081752s
1057+
Hash#values.select 28.417k (± 3.1%) i/s (35.19 μs/i) - 144.585k in 5.087922s
1058+
Hash#select.values 25.617k (± 3.7%) i/s (39.04 μs/i) - 130.200k in 5.082509s
1059+
1060+
Comparison:
1061+
Hash#values.compact: 598295.2 i/s
1062+
Hash#values.select: 28417.3 i/s - 21.05x slower
1063+
Hash#select.values: 25617.3 i/s - 23.36x slower
1064+
```
1065+
8991066
##### `Hash#merge!` vs `Hash#[]=` [code](code/hash/merge-bang-vs-\[\]=.rb)
9001067

9011068
```
@@ -1033,6 +1200,39 @@ Array#each_w/_object: 3890033.7 i/s - 2.71x slower
10331200
Hash#select-include : 1342942.2 i/s - 7.86x slower
10341201
```
10351202

1203+
##### `Hash#values_at` vs `Array#map { Hash#[] }` [code](code/hash/values_at-vs-map.rb)
1204+
1205+
To select hash values by keys, when some of the keys may not exist in the hash and you care about the default values.
1206+
1207+
```
1208+
$ ruby -v code/hash/values_at-vs-map.rb
1209+
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-darwin22]
1210+
Warming up --------------------------------------
1211+
Hash#values_at 503.783k i/100ms
1212+
Array#map { Hash#[] } 279.576k i/100ms
1213+
Calculating -------------------------------------
1214+
Hash#values_at 4.901M (± 4.1%) i/s - 24.685M in 5.046090s
1215+
Array#map { Hash#[] } 2.801M (± 3.1%) i/s - 14.258M in 5.095692s
1216+
1217+
Comparison:
1218+
Hash#values_at : 4900567.9 i/s
1219+
Array#map { Hash#[] }: 2800981.4 i/s - 1.75x slower
1220+
```
1221+
1222+
```
1223+
$ ruby -v code/hash/values_at-vs-map.rb
1224+
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [aarch64-linux]
1225+
Warming up --------------------------------------
1226+
Hash#values_at 669.893k i/100ms
1227+
Array#map { Hash#[] } 318.055k i/100ms
1228+
Calculating -------------------------------------
1229+
Hash#values_at 6.929M (± 7.2%) i/s (144.31 ns/i) - 34.834M in 5.027125s
1230+
Array#map { Hash#[] } 3.372M (± 5.1%) i/s (296.52 ns/i) - 17.175M in 5.092778s
1231+
1232+
Comparison:
1233+
Hash#values_at: 6929295.9 i/s
1234+
Array#map { Hash#[] }: 3372416.9 i/s - 2.05x slower
1235+
```
10361236

10371237
### Proc & Block
10381238

‎code/array/subset-check.rb‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require 'benchmark/ips'
2+
require 'set'
3+
4+
# Check whether ARRAY1 is a subset of ARRAY2 (every element of ARRAY1 is in ARRAY2).
5+
# The fastest approach depends on the input: `all?` + `include?` stops at the first miss (great for non-subsets) but is O(n*m) when ARRAY1 really is a subset, while Set-based lookups stay O(n).
6+
ARRAY1 = [*1..25]
7+
ARRAY2 = [*1..100]
8+
9+
# On CRuby 3.4 and newer (a1 - a2).empty? wins; on JRuby and TruffleRuby all? + include? does, and on 2.1 the two tie.
10+
def faster
11+
(ARRAY1 - ARRAY2).empty?
12+
end
13+
14+
def fast
15+
ARRAY1.all? { |element| ARRAY2.include?(element) }
16+
end
17+
18+
def slow
19+
(ARRAY1 & ARRAY2).size == ARRAY1.size
20+
end
21+
22+
def slower
23+
(ARRAY1 & ARRAY2) == ARRAY1
24+
end
25+
26+
def slowest
27+
ARRAY1.to_set.subset?(ARRAY2.to_set)
28+
end
29+
30+
# Sanity check: every approach must return the same answer.
31+
results = [faster, fast, slow, slower, slowest]
32+
raise "not equivalent: #{results.inspect}" unless results.uniq.size == 1
33+
34+
Benchmark.ips do |x|
35+
x.report('(a1 - a2).empty?') { faster }
36+
x.report('a1.all? { include? }') { fast }
37+
x.report('(a1 & a2).size == n') { slow }
38+
x.report('(a1 & a2) == a1') { slower }
39+
x.report('a1.to_set.subset?') { slowest }
40+
x.compare!
41+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
require 'benchmark/ips'
2+
3+
if RUBY_VERSION >= '2.4.0'
4+
ARRAY = (1..1000).to_a
5+
6+
# The two agree for Integers.
7+
# For Floats, Array#sum compensates for rounding errors: ([0.1] * 10).sum is 1.0, inject(:+) gives 0.9999999999999999.
8+
def fast
9+
ARRAY.sum
10+
end
11+
12+
def slow
13+
ARRAY.inject(:+)
14+
end
15+
16+
Benchmark.ips do |x|
17+
x.report('Array#sum') { fast }
18+
x.report('Array#inject(:+)') { slow }
19+
x.compare!
20+
end
21+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
require 'benchmark/ips'
2+
require 'forwardable'
3+
require 'delegate'
4+
5+
class AdvancedArray < SimpleDelegator
6+
def initialize(*args)
7+
@args = args
8+
self.__setobj__(@args)
9+
end
10+
11+
def push(value)
12+
@args.push(value)
13+
end
14+
15+
extend Forwardable
16+
def_delegator :@args, :push, :forwarded_push
17+
end
18+
19+
def fast
20+
array = AdvancedArray.new
21+
array.push(1) # Simple method call
22+
end
23+
24+
def slow
25+
array = AdvancedArray.new
26+
array.forwarded_push(1) # Forwarded method call
27+
end
28+
29+
def slower
30+
array = AdvancedArray.new
31+
array << 1 # Delegated method call (Array#<< through SimpleDelegator)
32+
end
33+
34+
Benchmark.ips do |x|
35+
x.report('method') { fast }
36+
x.report('forwarded method') { slow }
37+
x.report('delegated method') { slower }
38+
x.compare!
39+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
require 'benchmark/ips'
2+
3+
# A hash where every other value is nil, the same on every run, so every approach below returns the same result: the 500 non-nil values.
4+
HASH = Hash[(1..1000).map { |k| [k, k.even? ? k : nil] }]
5+
6+
def fast
7+
HASH.values.compact
8+
end
9+
10+
def slow
11+
HASH.values.select { |v| !v.nil? }
12+
end
13+
14+
def slower
15+
HASH.select { |_k, v| !v.nil? }.values
16+
end
17+
18+
# Sanity check: all three must return the same values, in the same order.
19+
raise 'not equivalent' unless [fast, slow, slower].uniq.size == 1
20+
21+
Benchmark.ips do |x|
22+
x.report('Hash#values.compact') { fast }
23+
x.report('Hash#values.select') { slow }
24+
x.report('Hash#select.values') { slower }
25+
x.compare!
26+
end

‎code/hash/values_at-vs-map.rb‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
require 'benchmark/ips'
2+
3+
HASH = {
4+
a: 'foo',
5+
b: 'bar',
6+
c: 'baz',
7+
d: 'qux'
8+
}.freeze
9+
10+
# Some of the keys may not exist in the hash; we want to keep the default values.
11+
KEYS = %i[a c e f].freeze
12+
13+
def fast
14+
HASH.values_at(*KEYS)
15+
end
16+
17+
def slow
18+
KEYS.map { |key| HASH[key] }
19+
end
20+
21+
Benchmark.ips do |x|
22+
x.report('Hash#values_at') { fast }
23+
x.report('Array#map { Hash#[] }') { slow }
24+
x.compare!
25+
end

0 commit comments

Comments
 (0)