Race track

Nicolas Hoizey

Performance Testing Enumerable’s Loveliness

After sharing my solution to Cassidy Williams’ oddSum challenge, Xavier & Alex suggested a simpler approach on social media. This got me curious: does avoiding the mathematical check actually improve performance?

So I decided to benchmark the solutions to find out.

require 'benchmark/ips'

n = 10
a = Array.new(n) { |i| rand(1_000_000) }
b = Array.new(n) { |i| rand(1_000_000) }

Benchmark.ips do |x|
  # Mine: pre-filter by odd/even, then compute products
  x.report('odd & even check') do
    (a.select(&:odd?).product(b.select(&:even?)) +
      b.select(&:odd?).product(a.select(&:even?)))
    .uniq
  end
  # Alternatives: compute all products, then filter by sum
  x.report('full product, +') do
    a.product(b).select { (_1 + _2).odd? }.uniq
  end
  x.report('full product, sum') do
    a.product(b).select { _1.sum.odd? }.uniq
  end

  x.compare!
end

In the testing script I set a variable (n) for the size of the generated arrays. I presumed that for small arrays there’d be very little difference it would be more noticeable as the arrays grew larger. Intuitively, I’d expect the performance to be worse when using the full product approach, as the memory allocation of the intermediate array would be much larger.

Here’s the results when n = 10:

Warming up --------------------------------------
    odd & even check     9.686k i/100ms
     full product, +     7.030k i/100ms
   full product, sum     6.911k i/100ms
Calculating -------------------------------------
    odd & even check     97.138k (± 0.7%) i/s   (10.29 μs/i) -    493.986k in   5.085694s
     full product, +     74.138k (± 1.8%) i/s   (13.49 μs/i) -    372.590k in   5.027459s
   full product, sum     69.031k (± 0.9%) i/s   (14.49 μs/i) -    345.550k in   5.006096s

Comparison:
    odd & even check:    97137.7 i/s
     full product, +:    74137.7 i/s - 1.31x  slower
   full product, sum:    69031.3 i/s - 1.41x  slower

A surprising difference in comparative performance, given the small size of the array, but the individual runtimes are very small; in the microseconds.

And for n = 1_000:

Warming up --------------------------------------
    odd & even check     1.000 i/100ms
     full product, +     1.000 i/100ms
   full product, sum     1.000 i/100ms
Calculating -------------------------------------
    odd & even check     12.568 (± 8.0%) i/s   (79.57 ms/i) -     63.000 in   5.023134s
     full product, +      6.891 (± 0.0%) i/s  (145.12 ms/i) -     35.000 in   5.091415s
   full product, sum      6.369 (± 0.0%) i/s  (157.00 ms/i) -     32.000 in   5.029184s

Comparison:
    odd & even check:       12.6 i/s
     full product, +:        6.9 i/s - 1.82x  slower
   full product, sum:        6.4 i/s - 1.97x  slower

And for n = 10_000:

Warming up --------------------------------------
    odd & even check     1.000 i/100ms
     full product, +     1.000 i/100ms
   full product, sum     1.000 i/100ms
Calculating -------------------------------------
    odd & even check      0.054 (± 0.0%) i/s    (18.57 s/i) -      1.000 in  18.574282s
     full product, +      0.021 (± 0.0%) i/s    (46.66 s/i) -      1.000 in  46.663944s
   full product, sum      0.022 (± 0.0%) i/s    (45.83 s/i) -      1.000 in  45.833142s

Comparison:
    odd & even check:        0.1 i/s
   full product, sum:        0.0 i/s - 2.47x  slower
     full product, +:        0.0 i/s - 2.51x  slower

The main surprise, which I didn’t initially consider, is how quickly the performance degrades with any of the approaches as the array sizes increase.

It makes sense, as that’s a factor of O(n²) (or near enough) in all solutions. Thousand element arrays mean one million pairs to evaluate. Whereas ten thousand element arrays is one hundred million pairs: a 100x increase! The use of .product ends up creating large intermediate arrays which increase memory usage.

In reality, for small arrays—as suggested in the original question—the performance difference is negligible, so picking the most readable solution is the most important decision. As the arrays get beyond the thousands of elements, were we to execute this task in a typical web request, none of the solutions are going to scale well.

For truly large datasets we might consider streaming, lazy evaluation or parallelization, but that’s outside the scope of this code golfing exercise.

Last updated on May 26th, 2025