Ruby: Proc#call vs yield

后端 未结 6 1459
野性不改
野性不改 2020-12-02 05:54

What are the behavioural differences between the following two implementations in Ruby of the thrice method?

module WithYield
  def self.thrice
         


        
相关标签:
6条回答
  • 2020-12-02 06:17

    BTW, just to update this to current day using:

    ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux]
    

    On Intel i7 (1.5 years oldish).

    user     system      total        real
    0.010000   0.000000   0.010000 (  0.015555)
    0.030000   0.000000   0.030000 (  0.024416)
    0.120000   0.000000   0.120000 (  0.121450)
    0.240000   0.000000   0.240000 (  0.239760)
    

    Still 2x slower. Interesting.

    0 讨论(0)
  • 2020-12-02 06:18

    The other answers are pretty thorough and Closures in Ruby extensively covers the functional differences. I was curious about which method would perform best for methods that optionally accept a block, so I wrote some benchmarks (going off this Paul Mucur post). I compared three methods:

    • &block in method signature
    • Using &Proc.new
    • Wrapping yield in another block

    Here is the code:

    require "benchmark"
    
    def always_yield
      yield
    end
    
    def sometimes_block(flag, &block)
      if flag && block
        always_yield &block
      end
    end
    
    def sometimes_proc_new(flag)
      if flag && block_given?
        always_yield &Proc.new
      end
    end
    
    def sometimes_yield(flag)
      if flag && block_given?
        always_yield { yield }
      end
    end
    
    a = b = c = 0
    n = 1_000_000
    Benchmark.bmbm do |x|
      x.report("no &block") do
        n.times do
          sometimes_block(false) { "won't get used" }
        end
      end
      x.report("no Proc.new") do
        n.times do
          sometimes_proc_new(false) { "won't get used" }
        end
      end
      x.report("no yield") do
        n.times do
          sometimes_yield(false) { "won't get used" }
        end
      end
    
      x.report("&block") do
        n.times do
          sometimes_block(true) { a += 1 }
        end
      end
      x.report("Proc.new") do
        n.times do
          sometimes_proc_new(true) { b += 1 }
        end
      end
      x.report("yield") do
        n.times do
          sometimes_yield(true) { c += 1 }
        end
      end
    end
    

    Performance was similar between Ruby 2.0.0p247 and 1.9.3p392. Here are the results for 1.9.3:

                      user     system      total        real
    no &block     0.580000   0.030000   0.610000 (  0.609523)
    no Proc.new   0.080000   0.000000   0.080000 (  0.076817)
    no yield      0.070000   0.000000   0.070000 (  0.077191)
    &block        0.660000   0.030000   0.690000 (  0.689446)
    Proc.new      0.820000   0.030000   0.850000 (  0.849887)
    yield         0.250000   0.000000   0.250000 (  0.249116)
    

    Adding an explicit &block param when it's not always used really does slow down the method. If the block is optional, do not add it to the method signature. And, for passing blocks around, wrapping yield in another block is fastest.

    That said, these are the results for a million iterations, so don't worry about it too much. If one method makes your code clearer at the expense of a millionth of a second, use it anyway.

    0 讨论(0)
  • 2020-12-02 06:25

    I found that the results are different depending on whether you force Ruby to construct the block or not (e.g. a pre-existing proc).

    require 'benchmark/ips'
    
    puts "Ruby #{RUBY_VERSION} at #{Time.now}"
    puts
    
    firstname = 'soundarapandian'
    middlename = 'rathinasamy'
    lastname = 'arumugam'
    
    def do_call(&block)
        block.call
    end
    
    def do_yield(&block)
        yield
    end
    
    def do_yield_without_block
        yield
    end
    
    existing_block = proc{}
    
    Benchmark.ips do |x|
        x.report("block.call") do |i|
            buffer = String.new
    
            while (i -= 1) > 0
                do_call(&existing_block)
            end
        end
    
        x.report("yield with block") do |i|
            buffer = String.new
    
            while (i -= 1) > 0
                do_yield(&existing_block)
            end
        end
    
        x.report("yield") do |i|
            buffer = String.new
    
            while (i -= 1) > 0
                do_yield_without_block(&existing_block)
            end
        end
    
        x.compare!
    end
    

    Gives the results:

    Ruby 2.3.1 at 2016-11-15 23:55:38 +1300
    
    Warming up --------------------------------------
              block.call   266.502k i/100ms
        yield with block   269.487k i/100ms
                   yield   262.597k i/100ms
    Calculating -------------------------------------
              block.call      8.271M (± 5.4%) i/s -     41.308M in   5.009898s
        yield with block     11.754M (± 4.8%) i/s -     58.748M in   5.011017s
                   yield     16.206M (± 5.6%) i/s -     80.880M in   5.008679s
    
    Comparison:
                   yield: 16206091.2 i/s
        yield with block: 11753521.0 i/s - 1.38x  slower
              block.call:  8271283.9 i/s - 1.96x  slower
    

    If you change do_call(&existing_block) to do_call{} you'll find it's about 5x slower in both cases. I think the reason for this should be obvious (because Ruby is forced to construct a Proc for each invocation).

    0 讨论(0)
  • 2020-12-02 06:35

    I think the first one is actually a syntactic sugar of the other. In other words there is no behavioural difference.

    What the second form allows though is to "save" the block in a variable. Then the block can be called at some other point in time - callback.


    Ok. This time I went and did a quick benchmark:

    require 'benchmark'
    
    class A
      def test
        10.times do
          yield
        end
      end
    end
    
    class B
      def test(&block)
        10.times do
          block.call
        end
      end
    end
    
    Benchmark.bm do |b|
      b.report do
        a = A.new
        10000.times do
          a.test{ 1 + 1 }
        end
      end
    
      b.report do
        a = B.new
        10000.times do
          a.test{ 1 + 1 }
        end
      end
    
      b.report do
        a = A.new
        100000.times do
          a.test{ 1 + 1 }
        end
      end
    
      b.report do
        a = B.new
        100000.times do
          a.test{ 1 + 1 }
        end
      end
    
    end
    

    The results are interesting:

          user     system      total        real
      0.090000   0.040000   0.130000 (  0.141529)
      0.180000   0.060000   0.240000 (  0.234289)
      0.950000   0.370000   1.320000 (  1.359902)
      1.810000   0.570000   2.380000 (  2.430991)
    

    This shows that using block.call is almost 2x slower than using yield.

    0 讨论(0)
  • 2020-12-02 06:36

    Here's an update for Ruby 2.x

    ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.3.0]

    I got sick of writing benchmarks manually so I created a little runner module called benchable

    require 'benchable' # https://gist.github.com/naomik/6012505
    
    class YieldCallProc
      include Benchable
    
      def initialize
        @count = 10000000    
      end
    
      def bench_yield
        @count.times { yield }
      end
    
      def bench_call &block
        @count.times { block.call }
      end
    
      def bench_proc &block
        @count.times &block
      end
    
    end
    
    YieldCallProc.new.benchmark
    

    Output

                          user     system      total        real
    bench_yield       0.930000   0.000000   0.930000 (  0.928682)
    bench_call        1.650000   0.000000   1.650000 (  1.652934)
    bench_proc        0.570000   0.010000   0.580000 (  0.578605)
    

    I think the most surprising thing here is that bench_yield is slower than bench_proc. I wish I had a little more of an understanding for why this is happening.

    0 讨论(0)
  • 2020-12-02 06:37

    They give different error messages if you forget to pass a block:

    > WithYield::thrice
    LocalJumpError: no block given
            from (irb):3:in `thrice'
            from (irb):3:in `times'
            from (irb):3:in `thrice'
    
    > WithProcCall::thrice
    NoMethodError: undefined method `call' for nil:NilClass
            from (irb):9:in `thrice'
            from (irb):9:in `times'
            from (irb):9:in `thrice'
    

    But they behave the same if you try to pass a "normal" (non-block) argument:

    > WithYield::thrice(42)
    ArgumentError: wrong number of arguments (1 for 0)
            from (irb):19:in `thrice'
    
    > WithProcCall::thrice(42)
    ArgumentError: wrong number of arguments (1 for 0)
            from (irb):20:in `thrice'
    
    0 讨论(0)
提交回复
热议问题