2009-09-11 29 views
67

Sự khác nhau về hành vi giữa hai triển khai sau trong Ruby của phương thức thrice là gì?Ruby: ProC# call vs yield

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

Bằng cách "ứng xử" tôi bao gồm xử lý lỗi, hiệu suất, công cụ hỗ trợ, vv

+5

Lưu ý phụ: 'def thrice (& block)' là tự ghi nhiều tài liệu hơn, đặc biệt là so với 'yield' được chôn ở đâu đó trong một phương thức lớn. –

Trả lời

46

Tôi nghĩ là người đầu tiên thực sự là một đường cú pháp của người kia. Nói cách khác không có sự khác biệt về hành vi.

Biểu mẫu thứ hai cho phép mặc dù là "lưu" khối đó trong một biến. Sau đó, khối có thể được gọi tại một số điểm khác trong thời gian - gọi lại.


Ok. Lần này tôi đã đi và đã làm một điểm chuẩn nhanh:

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 

Kết quả là thú vị:

 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) 

Điều này cho thấy rằng việc sử dụng block.call gần như 2x chậm hơn so với sử dụng năng suất.

+7

Tôi nghĩ Ruby sẽ nhất quán hơn nếu điều đó là đúng (tức là nếu 'yield' chỉ là cú pháp cú pháp cho' ProC# call') nhưng tôi không nghĩ đó là sự thật. ví dụ. có hành vi xử lý lỗi khác nhau (xem câu trả lời của tôi bên dưới). Tôi cũng đã thấy đề xuất (ví dụ: http://stackoverflow.com/questions/764134/rubys-yield-feature-in-relation-to-computer-science/765126#765126) rằng 'yield' hiệu quả hơn, bởi vì trước tiên nó không phải tạo đối tượng 'Proc' và sau đó gọi phương thức' call' của nó. –

+0

Cập nhật lại với điểm chuẩn: vâng, tôi cũng đã làm một số điểm chuẩn và nhận được 'ProC# call' là _more_ gấp 2 lần so với 'yield', trên MRI 1.8.6p114. Trên JRuby (1.3.0, JVM 1.6.0_16 Server VM) sự khác biệt thậm chí còn nổi bật hơn: 'ProC# call' là khoảng * 8x * chậm như' yield'. Điều đó nói rằng, 'yield' trên JRuby nhanh hơn gấp đôi so với 'yield' trên MRI. –

+0

Tôi đã khai thác trên MRI 1.8.7p174 x86_64-linux. – jpastuszek

5

Họ thông báo lỗi khác nhau nếu bạn quên đi một khối:

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

Nhưng họ cư xử như vậy nếu bạn cố gắng để vượt qua một "bình thường" (không-block) lập luận:

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

Sự khác biệt về hành vi giữa các loại khác nhau của ruby ​​đóng cửa has been extensively documented

+1

Đó là một liên kết tốt - sẽ phải đọc chi tiết sau. Cảm ơn! –

+0

Một số thông tin khác, cụ thể về ký hiệu đơn nhất, nhưng trong hiểu biết rằng bạn cũng sẽ hiểu được sự khác biệt. http://weblog.raganwald.com/2008/06/what-does-do-when-used-as-unary.html – scragz

0

BTW, chỉ để cập nhật này cho đến ngày hiện tại sử dụng:

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

Trên Intel i7 (1,5 năm tuổi).

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) 

Vẫn còn chậm hơn 2x. Hấp dẫn.

9

Dưới đây là một bản cập nhật cho Ruby 2.x

ruby ​​2.0.0p247 (2013/06/27 sửa đổi 41.674) [x86_64-darwin12.3.0]

tôi bị ốm viết chuẩn bằng tay nên tôi đã tạo ra một mô-đun Á hậu chút gọi là 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) 

Tôi nghĩ điều đáng ngạc nhiên nhất ở đây là bench_yield chậm hơn bench_proc. Tôi ước gì tôi hiểu thêm một chút về lý do tại sao điều này xảy ra.

+2

Tôi tin rằng điều này là do trong 'bench_proc' toán tử đơn nguyên thực sự chuyển proc thành khối của lệnh' times', bỏ qua phí tạo khối cho 'lần' trong' bench_yield' và 'bench_call'. Đây là một loại trường hợp đặc biệt kỳ lạ, có vẻ như 'yield' vẫn nhanh hơn trong hầu hết các trường hợp. Thông tin thêm về proc để chặn chuyển nhượng: http://ablogaboutcode.com/2012/01/04/the-ampersand-operator-in-ruby/ (phần: Unary &) –

+0

'Số nguyên số lần gọi là' yield' (phiên bản c, rb_yield, trong đó có một VALUE đại diện cho một khối). Đó là lý do tại sao bench_proc quá nhanh. –

3

Các câu trả lời khác khá kỹ lưỡng và Closures in Ruby bao quát rộng rãi các khác biệt chức năng.Tôi đã tò mò về phương pháp nào sẽ thực hiện tốt nhất cho các phương pháp mà tùy chọn chấp nhận một khối, vì vậy tôi đã viết một số tiêu chuẩn (sẽ tắt this Paul Mucur post). Tôi so sánh ba phương pháp:

  • & khối trong phương pháp chữ ký
  • Sử dụng &Proc.new
  • Bao bì yield trong một khối

Đây là mã:

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 

Hiệu suất tương tự giữa Ruby 2.0.0p247 và 1.9.3p392. Dưới đây là các kết quả cho 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) 

Thêm một rõ ràng &block param khi nó không phải lúc nào cũng sử dụng thực sự làm chậm phương pháp. Nếu khối là tùy chọn, không thêm nó vào chữ ký phương thức. Và, để truyền các khối xung quanh, gói yield trong khối khác là nhanh nhất.

Điều đó nói rằng, đây là kết quả cho một triệu lần lặp lại, do đó, đừng lo lắng về nó quá nhiều. Nếu một phương pháp làm cho mã của bạn rõ ràng hơn với chi phí bằng một phần triệu giây, hãy sử dụng nó.

1

Tôi thấy rằng các kết quả khác nhau tùy thuộc vào việc bạn có buộc Ruby xây dựng khối hay không (ví dụ: một proc đã tồn tại trước đó).

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 

Cung cấp kết quả:

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 

Nếu bạn thay đổi do_call(&existing_block)-do_call{} bạn sẽ thấy đó là khoảng 5x chậm hơn trong cả hai trường hợp. Tôi nghĩ lý do cho điều này nên rõ ràng (bởi vì Ruby buộc phải xây dựng một Proc cho mỗi yêu cầu).