Ruby:Proc#call与收益


77

以下两种在Ruby中实现该thrice方法的行为差异是什么?

module WithYield
  def self.thrice
    3.times { yield }      # yield to the implicit block argument
  end
end

module WithProcCall
  def self.thrice(&block)  # & converts implicit block to an explicit, named Proc
    3.times { block.call } # invoke Proc#call
  end
end

WithYield::thrice { puts "Hello world" }
WithProcCall::thrice { puts "Hello world" }

通过“行为差异”,我包括错误处理,性能,工具支持等。


不同类型的红宝石
封盖

8
旁注:def thrice(&block)具有更多的自我记录性,尤其是与yield采用大型方法掩埋的地方相比。
内森·朗

Answers:


51

我认为第一个实际上是另一个的语法糖。换句话说,没有行为上的差异。

第二种形式允许的是将块“保存”在变量中。然后可以在其他时间点调用该块-回调。


好。这次我去做了一个快速基准测试:

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

结果很有趣:

      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)

这表明使用block.call几乎比使用yield慢2


8
我认为Ruby如果是正确的话(例如,如果yield只是使用语法糖的话Proc#call)会更加一致,但是我认为这不是真的。例如,存在不同的错误处理行为(请参见下面的答案)。我也看到它建议(例如stackoverflow.com/questions/764134/…yield更有效,因为它不必先创建一个Proc对象然后调用其call方法。
山姆·斯托克斯

再有基准更新:是的,我做了一些基准测试过,并得到Proc#call比2X一样慢yield,在MRI 1.8.6p114。在JRuby(1.3.0,JVM 1.6.0_16 Server VM)上,差异更加明显:Proc#call大约慢8倍yield。也就是说,yield在JRuby上的速度是yield在MRI上的两倍。
山姆·斯托克斯

我在MRI 1.8.7p174 x86_64-linux上进行了挖掘。
jpastuszek,2009年

3
您还缺少第三个案例:def test(&block) ; 10.times(&block) ; end,它应该与yield案例进行相同的测试。
抢劫

1
以上基准也近似于Ruby v2.1.2;block.call比慢约1.7倍yield
加夫2014年

9

这是Ruby 2.x的更新

红宝石2.0.0p247(2013-06-27修订版41674)[x86_64-darwin12.3.0]

我厌倦了手动编写基准测试,因此创建了一个称为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

输出量

                      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)

我认为这里最令人惊讶的是,bench_yield它的速度比慢bench_proc。我希望我对为什么会有所了解。


2
我相信这是因为在bench_proc一元运算符中,实际上是将proc转换为times调用的块,从而跳过了timesinbench_yield和的块创建的开销bench_call。这是一种特殊情况的怪异用法,yield在大多数情况下看起来还是比较快的。有关proc阻止分配的详细信息:ablogaboutcode.com/2012/01/04/the-ampersand-operator-in-ruby(部分:The Unary&)
Matt Sanders

Integer#times调用yield(C版本rb_yield,它采用代表一个块的VALUE)。这就是bench_proc这么快的原因。
Nate Symer

6

如果您忘记传递一个块,它们会给出不同的错误消息:

> 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'

但是,如果您尝试传递“正常”(非阻塞)参数,它们的行为相同:

> 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'

6

其他答案非常彻底,Ruby中的Closures广泛涵盖了功能差异。我很好奇哪种方法对于可以接受块的方法最有效,所以我写了一些基准测试(参考Paul Mucur的文章)。我比较了三种方法:

  • &阻止方法签名
  • 使用 &Proc.new
  • 包装yield在另一个块中

这是代码:

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

Ruby 2.0.0p247和1.9.3p392之间的性能相似。这是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)

&block不经常使用的情况下添加显式参数确实会使该方法变慢。如果该块是可选的,则不要将其添加到方法签名中。而且,要在块之间传递,包裹yield在另一个块中最快。

就是说,这些是一百万次迭代的结果,所以不必太担心。如果一种方法花费一秒钟的百万分之一来使代码更清晰,则无论如何都要使用它。


2

我发现结果是不同的,具体取决于您是否强制Ruby构造该块(例如,预先存在的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

给出结果:

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

如果您将其更改do_call(&existing_block)do_call{}两种情况,则会发现速度要慢5倍左右。我认为这样做的原因应该很明显(因为Ruby被迫为每个调用构造一个Proc)。


0

顺便说一句,只是使用以下方法将其更新为当前日期:

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

在Intel i7(已使用1.5年)上。

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)

仍然慢2倍。有趣。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.