Ruby:Proc#call 与 Yield

发布于 2024-08-04 01:09:59 字数 481 浏览 7 评论 0原文

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

我所说的“行为差异”包括错误处理、性能、工具支持等。

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

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

By "behavioural differences" I include error handling, performance, tool support, etc.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

寂寞笑我太脆弱 2024-08-11 01:09:59

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

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


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

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 倍。

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.

水波映月 2024-08-11 01:09:59

其他答案非常彻底,Closures in Ruby 广泛涵盖了功能差异。我很好奇哪种方法对于可选接受块的方法表现最好,因此我编写了一些基准测试(离开这篇 Paul Mucur 帖子)。我比较了三种方法:

  • &block 在方法签名中
  • 使用 &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 和 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 包装在另一个块中是最快的。

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

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.

十雾 2024-08-11 01:09:59

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

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

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'
尽揽少女心 2024-08-11 01:09:59

我发现结果会有所不同,具体取决于您是否强制 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.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)。

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).

第几種人 2024-08-11 01:09:59

顺便说一句,只是为了将其更新到今天,使用:

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 倍。有趣的。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文