2009-09-11 18 views
67

Jakie są różnice behawioralne między dwiema poniższymi implementacjami w Rubim metody thrice?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" } 

Przez „różnice behawioralne” I obejmują obsługę błędów, wydajność, wsparcie narzędzie, itp

+5

Nota boczna: 'def trzykrotnie (& blok)' jest bardziej samo-dokumentujące, szczególnie w stosunku do 'plonu' pochowanego gdzieś w dużej metodzie. –

Odpowiedz

46

Myślę, że pierwszy z nich jest faktycznie syntaktyczne cukru z drugiej. Innymi słowy, nie ma różnicy w zachowaniu.

To, na co pozwala druga forma, to "zapisać" blok w zmiennej. Następnie blok może zostać wywołany w innym momencie - callback.


Ok. Tym razem poszedłem i zrobiłem szybki odniesienia:

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 

Wyniki są interesujące:

 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) 

To pokazuje, że przy użyciu block.call jest prawie 2x wolniej niż przy użyciu wydajność.

+7

Myślę, że Ruby byłaby bardziej spójna, gdyby to była prawda (tj. Jeśli 'yield' byłby po prostu syntaktycznym cukrem dla' ProC# call'), ale nie sądzę, że to prawda. na przykład istnieje różne zachowanie podczas obsługi błędów (zobacz moją odpowiedź poniżej). Widziałem także to sugerowane (np. Http://stackoverflow.com/questions/764134/rubys-yield-feature-in-relation-to-computer-science/765126 # 765126), że 'wydajność' jest bardziej wydajna, ponieważ nie musi najpierw tworzyć obiektu 'Proc', a następnie wywoływać jego metodę' call'. –

+0

Po aktualizacji z testami porównawczymi: tak, zrobiłem też testy porównawcze i otrzymałem 'ProC# call' będący _more_ niż 2x tak wolny jak' yield', na MRI 1.8.6p114. W JRuby (1.3.0, JVM 1.6.0_16 Server VM) różnica była jeszcze bardziej uderzająca: 'ProC# call' było około * 8x * tak wolne jak' yield'. To powiedziawszy, "wydajność" na JRuby była dwukrotnie szybsza niż "wydajność" w MRI. –

+0

Zrobiłem moje na MRI 1.8.7p174 x86_64-linux. – jpastuszek

5

Dają różne komunikaty o błędach, jeśli zapomni przekazać blok:

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

Ale oni zachowują się tak samo, jeśli starają się przekazać „normalne” (non-blokowy) 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' 
23

behawioralne różnica pomiędzy różnymi typami rubin closures has been extensively documented

+1

To dobre łącze - będzie trzeba je później szczegółowo przeczytać. Dzięki! –

+0

Jeszcze trochę informacji, w szczególności na temat unrsalnego znaku ampersand, ale ze zrozumieniem, że również zrozumiesz różnice. http://weblog.raganwald.com/2008/06/what-does-do-when-used-as-unary.html – scragz

0

BTW, po prostu zaktualizować to do bieżącego dnia, używając:

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

Na Intel i7 (1,5 lat).

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) 

Jeszcze 2x wolniej. Ciekawy.

9

Oto aktualizacja dla Ruby 2.x

rubin 2.0.0p247 (27.06.2013 rewizja 41674) [x86_64-darwin12.3.0]

mam dość pisania wzorców ręcznie więc stworzyłem mały moduł biegacz nazwie 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 

Wyjście

     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) 

Myślę, że najbardziej zaskakujące jest to, że bench_yield jest wolniejsze niż bench_proc. Chciałbym mieć trochę więcej zrozumienia, dlaczego tak się dzieje.

+2

Wierzę, że dzieje się tak dlatego, że w 'bench_proc' operator jednoargumentowy faktycznie przekształca proc w blok komendy' times', pomijając narzut tworzenia bloku dla 'times' w' bench_yield' i 'bench_call'. Jest to dziwny rodzaj specjalnego zastosowania, wygląda na to, że "wydajność" jest nadal szybsza w większości przypadków. Więcej informacji o proc, aby zablokować zadanie: http://abogaboutcode.com/2012/01/04/the-ampersand-operator-in-ruby/ (sekcja: The Unary &) –

+0

'Integer # times' call' yield' (wersja c, rb_yield, która przyjmuje wartość VALUE reprezentującą blok). Właśnie dlatego bench_proc jest tak szybki. –

3

Pozostałe odpowiedzi są dość dokładne, a Closures in Ruby obszernie omawia różnice funkcjonalne.Byłem ciekawy, która metoda byłaby najlepsza dla metod, które akceptują blok , opcjonalnie, więc napisałem kilka testów porównawczych (wyłączając this Paul Mucur post). I w porównaniu z trzech metod:

  • & blok w metodzie podpisu
  • Korzystanie &Proc.new
  • Owijanie yield w innym bloku

Oto kod:

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 

wydajność była podobna między Ruby 2.0.0p247 i 1.9.3p392. Oto wyniki dla 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) 

dodanie wyraźnego &block param kiedy to nie zawsze naprawdę nie spowolnić metody. Jeśli blok jest opcjonalny, nie dodawaj go do podpisu metody. I, dla przechodzenia bloków wokół, owijanie yield w innym bloku jest najszybsze.

To powiedziawszy, są to wyniki dla miliona powtórzeń, więc nie przejmuj się tym zbytnio. Jeśli jedna metoda uczyni twój kod bardziej przejrzystym kosztem jednej milionowej sekundy, użyj go w każdym razie.

1

Znalazłem, że wyniki są różne w zależności od tego, czy wymuszasz na Ruby blokowanie, czy nie (np. Wcześniej istniejący proces).

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 

podaje wyniki:

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 

Jeżeli zmienisz do_call(&existing_block) do do_call{} znajdziesz to około 5x wolniej w obu przypadkach. Myślę, że powód tego powinien być oczywisty (ponieważ Ruby jest zmuszona do skonstruowania Proc dla każdej inwokacji).