2009-09-11 13 views
67

Quali sono le differenze comportamentali tra le seguenti due implementazioni in Ruby del metodo 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" } 

Con "differenze comportamentali" Io sono la gestione degli errori, le prestazioni, strumenti di supporto, ecc

+5

Nota a margine: 'def throry (& block)' è più autodocumentante, in particolare rispetto a un 'yield' sepolto da qualche parte in un metodo di grandi dimensioni. –

risposta

46

penso che il primo è in realtà uno zucchero sintattico dell'altro. In altre parole non c'è differenza comportamentale.

Ciò che il secondo modulo consente è di "salvare" il blocco in una variabile. Quindi il blocco può essere chiamato in un altro momento - callback.


Ok. Questa volta sono andato e ha fatto un rapido punto di riferimento:

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 

I risultati sono interessanti:

 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) 

Questo dimostra che l'uso di block.call è quasi 2 volte più lento rispetto all'utilizzo di resa.

+7

Penso che Ruby sarebbe più coerente se fosse vero (cioè se "yield" fosse solo zucchero sintattico per "ProC# call") ma non penso sia vero. per esempio. c'è il diverso comportamento nella gestione degli errori (vedi la mia risposta sotto). Ho anche visto suggerito (es. Http://stackoverflow.com/questions/764134/rubys-yield-feature-in-relation-to-computer-science/765126#765126) che 'yield' è più efficiente, perché non deve prima creare un oggetto 'Proc' e quindi richiamare il suo metodo' call'. –

+0

Aggiornamento con benchmark: sì, ho fatto anche alcuni benchmark e ho ottenuto che "ProC# call" fosse _more_ di 2x più lento di "yield", su MRI 1.8.6p114. Su JRuby (1.3.0, JVM 1.6.0_16 Server VM) la differenza era ancora più sorprendente: "ProC# call" era di circa * 8x * lento come "yield". Detto questo, 'yield' su JRuby era due volte più veloce di' yield' sulla risonanza magnetica. –

+0

Ho fatto il mio su MRI 1.8.7p174 x86_64-linux. – jpastuszek

5

danno diversi messaggi di errore se si dimentica di passare un blocco:

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

Ma si comportano lo stesso se si tenta di passare un "normale" (non-blocco) argomento:

> 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

La differenza di comportamento tra i diversi tipi di chiusure rubino has been extensively documented

+1

Questo è un buon collegamento - dovrà leggerlo in dettaglio più tardi. Grazie! –

+0

Altre informazioni, in particolare sulla e commerciale unitaria, ma comprendendo che anche tu capiresti le differenze. http://weblog.raganwald.com/2008/06/what-does-do-when-used-as-unary.html – scragz

0

BTW, proprio per aggiornare questo per giorno corrente utilizzando:

0.123.516,410617 millions
ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux] 

Su Intel i7 (1,5 anni 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) 

Ancora 2 volte più lento. Interessante.

9

Ecco un aggiornamento per Ruby 2.x

rubino 2.0.0p247 (2013/06/27 revisione 41674) [x86_64-darwin12.3.0]

mi sono ammalato di scrittura benchmark manualmente così ho creato un po modulo corridore chiamato 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 

uscita

     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) 

Penso che la cosa più sorprendente qui è che bench_yield è più lento di bench_proc. Vorrei avere un po 'più di comprensione del perché questo sta accadendo.

+2

Credo che questo sia dovuto al fatto che in 'bench_proc' l'operatore unario sta effettivamente trasformando il proc nel blocco della chiamata' times', saltando il sovraccarico della creazione del blocco per i 'times' in' bench_yield' e 'bench_call'. Questo è un tipo strano di uso del caso speciale, sembra che 'yield' è ancora più veloce nella maggior parte dei casi. Ulteriori informazioni su proc to block assignment: http://ablogaboutcode.com/2012/01/04/the-ampersand-operator-in-ruby/ (sezione: The Unary &) –

+0

'Integer # times' chiama' yield' (la versione c, rb_yield, che prende un VALUE che rappresenta un blocco). Ecco perché bench_proc è così veloce. –

3

Le altre risposte sono piuttosto approfondite e Closures in Ruby copre ampiamente le differenze funzionali.Ero curioso di sapere quale metodo avrebbe funzionato al meglio per i metodi che facoltativamente accetta un blocco, quindi ho scritto alcuni benchmark (in corso this Paul Mucur post). Ho confrontato tre metodi:

  • & blocco nella firma del metodo
  • utilizzando &Proc.new
  • Wrapping yield in un altro blocco

Ecco il codice:

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 è risultata simile tra Ruby 2.0.0p247 e 1.9.3p392. Ecco i risultati per: 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) 

l'aggiunta di un esplicito &block param quando non è sempre utilizzato in realtà non rallentare il metodo. Se il blocco è facoltativo, non aggiungerlo alla firma del metodo. E, per passare i blocchi in giro, avvolgere yield in un altro blocco è più veloce.

Detto questo, questi sono i risultati per un milione di iterazioni, quindi non preoccuparti troppo. Se un metodo rende il tuo codice più chiaro a scapito di un milionesimo di secondo, usalo comunque.

1

Ho trovato che i risultati sono diversi a seconda se forzate Ruby a costruire il blocco o meno (ad esempio un proc preesistente).

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 

dà i risultati:

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 

Se si cambia do_call(&existing_block)-do_call{} troverete che è circa 5 volte più lento in entrambi i casi. Penso che la ragione di ciò dovrebbe essere ovvia (perché Ruby è costretto a costruire un Proc per ogni invocazione).