2

Warning: This has little practical value. I just want to know what's going on.

I've come accross this line multiple times online:

return to_enum __method__ unless block_given?

I wanted to test it out and made a generator method with it (#1), then I tried without it and came up with an enumerator (#2). I was happy with that, thinking that's the way it should be done - though later I came up with a solution using #lazy (#3) which I think looks more elegant.

Can you guess which is fastest? To my surprise, #1 is the fastest, followed by #2 and #3!
To me, the first looks like a bit of a hack and has undesirable behavior such as stalling if you give it an empty block (rather than just throwing an error if a block is given).

My QUESTION is, what's the fastest way to do this? How come #1 is faster than #2, what am I missing? If my solutions are okay, which is objectively best?

Edit: Simpler example, was fizzbuzz before (http://pastebin.com/kXbbfxBc)

def method
  return to_enum __method__ unless block_given?
  n = 0
  loop do
    n += 1
    yield n ** 2
  end
end

enum = Enumerator.new do |yielder|
  n = 0
  loop do
    n += 1
    yielder.yield n ** 2
  end
end

lazy = (1..Float::INFINITY).lazy.map do |n|
  n ** 2
end

p method.take 50
p enum.take 50
p lazy.first 50

require 'benchmark/ips'
Benchmark.ips do |bm|
  bm.report('Method'){ method.take 50 }
  bm.report('Enumerator'){ enum.take 50 }
  bm.report('Lazy'){ lazy.first 50 }
  bm.compare!
end
user21033168
  • 444
  • 1
  • 4
  • 13
  • 1
    Please put your question into the question instead of posting the most important part of it on some random website. – Jörg W Mittag Feb 12 '15 at 12:12
  • 1
    @JörgWMittag The pastebin? Hardly a random website, it's 68 lines - too much to put in the question surely. If you mean the title, I struggled with wording, I don't mind if you edit it. – user21033168 Feb 12 '15 at 12:19
  • @user21033168 maybe you can come up with a shorter example. I don't think your question is related to the fizz-buzz algorithm, is it? – Stefan Feb 12 '15 at 14:01

1 Answers1

6

The latter two forms both have block bindings, which does have some overhead; when you create an Enumerator with a block, Ruby converts the block to a Proc and assigns it to an Enumerator::Generator, which then iterates by calling the proc. This has overhead that calling a method directly doesn't.

If we eliminate the block forms, the performance penalty is also eliminated:

def method
  return to_enum __method__ unless block_given?
  n = 0
  loop do
    n += 1
    yield n ** 2
  end
end

def method_sans_enum
  n = 0
  loop do
    n += 1
    yield n ** 2
  end
end

method_enum = Enumerator.new(self, :method_sans_enum)

enum = Enumerator.new do |yielder|
  n = 0
  loop do
    n += 1
    yielder.yield n ** 2
  end
end

lazy = (1..Float::INFINITY).lazy.map do |n|
  n ** 2
end

p method.take 50
p enum.take 50
p method_enum.take 50
p lazy.first 50

require 'benchmark/ips'
Benchmark.ips do |bm|
  bm.report('Method'){ method.take 50 }
  bm.report('Enumerator'){ enum.take 50 }
  bm.report('Enumerator 2'){ method_enum.take 50 }
  bm.report('Lazy'){ lazy.first 50 }
  bm.compare!
end

And results:

      Method    10.874k i/100ms
  Enumerator     6.152k i/100ms
Enumerator 2    11.733k i/100ms
        Lazy     3.885k i/100ms

Comparison:
        Enumerator 2:   132050.2 i/s
              Method:   124784.1 i/s - 1.06x slower
          Enumerator:    65961.9 i/s - 2.00x slower
                Lazy:    40063.6 i/s - 3.30x slower

Invoking procs involves overhead that invoking methods doesn't; for example:

class Foo
  def meth; end
end

instance = Foo.new
pr = instance.method(:meth).to_proc

require 'benchmark/ips'
Benchmark.ips do |bm|
  bm.report('Method'){ instance.meth }
  bm.report('Proc'){ pr.call }
  bm.compare!
end

Results:

Calculating -------------------------------------
          Method   121.016k i/100ms
            Proc   104.612k i/100ms
-------------------------------------------------
          Method      6.823M (± 0.1%) i/s -     34.127M
            Proc      3.443M (± 6.4%) i/s -     17.156M

Comparison:
          Method:  6822666.0 i/s
            Proc:  3442578.2 i/s - 1.98x slower

Calling a method that has been converted to a proc is 2x slower than calling the method directly - just nearly the exact performance deviation that you observed.

Chris Heald
  • 61,439
  • 10
  • 123
  • 137
  • Nice, not only faster but behaves like a normal enum. I'm using `method_enum = to_enum :method_sans_enum` to avoid a warning. It's interesting to see this is the fastest way, thx. – user21033168 Feb 13 '15 at 01:51
  • I also found `while true` is way faster than `loop do`! – user21033168 Feb 13 '15 at 02:18