Ruby:Proc#调用vs yield

Ruby的三种方法在以下两种实现之间的行为差​​异是什么?

 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" } 

“行为差异”包括error handling,性能,工具支持等。

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

第二种forms允许的是将块保存在variables中。 然后可以在其他时间点调用该块 – callback。


好。 这一次我去了一个快速的基准:

 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要慢两倍。

不同types的ruby瓶盖之间的行为差​​异已被广泛logging

这是Ruby 2.x的更新

ruby2.0.0p247(2013-06-27修订号41674)[x86_64-darwin12.3.0]

我厌倦了手动编写基准testing,所以我创build了一个名为benable的小亚军模块

 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_yieldbench_proc慢。 我希望我多了解一下为什么会发生这种情况。

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

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

其他答案是非常彻底的, Ruby中的闭包广泛地涵盖了function差异。 我很好奇哪种方法最适合可选地接受块的方法,所以我写了一些基准(关于这个Paul Mucur的post )。 我比较了三种方法:

  • 阻止方法签名
  • 使用&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 param参数确实会减慢这个方法的速度。 如果该块是可选的,则不要将其添加到方法签名中。 而且,为了传递块,在另一个块中包装yield是最快的。

也就是说,这些是一百万次迭代的结果,所以不要太担心。 如果有一种方法让你的代码更加清晰,而且代价是百万分之一秒,那就使用它。

我发现结果是不同的,取决于你是否强迫Ruby构造块(例如,一个预先存在的过程)。

 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.502ki/100ms yield with block 269.487ki/100ms yield 262.597ki/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)。

顺便说一句,只是为了更新到当前使用:

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

在英特尔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) 

仍然慢两倍。 有趣。