5

I'm trying to process a large list of numbers:

require 'benchmark'

N = 999999

Benchmark.bm 10 do |bm|
   bm.report 'Eager:' do
 (0..N).select(&:even?).map{|x| x * x}.reduce(&:+)
   end  
   bm.report 'Lazy:' do
 (0..N).lazy.select(&:even?).map{|x| x * x}.reduce(&:+)
   end  
 end;

To my understanding, the lazy version should be much faster, because the eager version needs to allocate two lists of half a million items each(one for select and one for map) while the lazy version is streaming everything.

However, when I run it, the lazy version takes more than twice as long as the eager one! (http://rextester.com/OTEX7399)

         user     system      total        real
Eager:       0.210000   0.010000   0.220000 (  0.216572)
Lazy:        0.580000   0.000000   0.580000 (  0.635091)

How can it be?

Idan Arye
  • 12,402
  • 5
  • 49
  • 68
  • Allocation of a memory does not affect the performance at all. It’s a single CPU instruction. I would bet the issue is in `reduce` that cannot be efficient for lazy operations, but in general the difference “two times faster” does basically mean “the execution time equals.” – Aleksei Matiushkin Dec 05 '17 at 12:33

1 Answers1

12

I'd say an Enumerator is a far slower middle man than memory is.

This was also reported a while back and Ruby core team member Yusuke Endoh said:

Enumerator::Lazy is not a silver bullet; it removes the overhead for creating an intermediate array, but brings the drawback for calling a block. Unfortunately, the latter is much bigger than the former. Thus, in general, Lazy does bring performance drawback.


An analogy I just thought of: Imagine you're building some furniture for a friend.

  • Non-lazy: You build the whole thing, rent a truck, and drive it to your friend.

  • Lazy: You build a little piece and drive it to your friend with your car. You build the next little piece and drive it to your friend with your car. You build the next little piece and drive it to your friend with your car. And so on.

Yes, renting that truck is extra overhead, but it's nothing compared to driving over and over again with your car.

The real reason that being lazy can save time is that after the first few pieces your friend finds out that you slept with his wife so now you're no longer friends and he doesn't want your stupid furniture anymore and you're not building the remaining pieces at all.

Stefan Pochmann
  • 27,593
  • 8
  • 44
  • 107
  • 1
    In your [linked thread](https://bugs.ruby-lang.org/issues/6183), it seems that the Ruby team did attempt to juice some performance out of lazy enumerators in [Revision 56158](https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/56185). Is there any way to know which Ruby version applies that revision? The feature number doesn't seem to show up on the roadmap – David Bodow Aug 07 '18 at 17:17
  • It was improved to be 30~40% faster: http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/77130 – Khalil Gharbaoui Aug 23 '19 at 09:07