pass blocks argument ruby yield

blocks - ruby pass block as argument



Ruby: Proc#call vs yield (7)

Aquí hay una actualización para Ruby 2.x

ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.3.0]

Me cansé de escribir puntos de referencia manualmente, así que creé un pequeño módulo de corredor llamado 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

Salida

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)

Creo que lo más sorprendente aquí es que bench_yield es más lento que bench_proc . Desearía tener un poco más de comprensión sobre por qué sucede esto.

¿Cuáles son las diferencias de comportamiento entre las dos implementaciones siguientes en el método Ruby of the 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" }

Por "diferencias de comportamiento" incluyo manejo de errores, rendimiento, soporte de herramientas, etc.


Creo que el primero es en realidad un azúcar sintáctico del otro. En otras palabras, no hay diferencia de comportamiento.

Sin embargo, lo que permite la segunda forma es "guardar" el bloque en una variable. Luego se puede llamar al bloque en algún otro punto en el tiempo: devolución de llamada.

De acuerdo. Esta vez fui e hice un punto de referencia rápido:

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

Los resultados son interesantes:

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)

Esto muestra que usar block.call es casi 2 veces más lento que usar yield .


Dan diferentes mensajes de error si olvida pasar un bloque:

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

Pero se comportan igual si intentas pasar un argumento "normal" (sin bloque):

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


Descubrí que los resultados son diferentes dependiendo de si fuerza a Ruby a construir el bloque o no (por ejemplo, un proceso preexistente).

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

Da los resultados:

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

Si cambia do_call(&existing_block) a do_call{} , encontrará que es aproximadamente 5 veces más lento en ambos casos. Creo que la razón de esto debería ser obvia (porque Ruby se ve obligada a construir un Proc para cada invocación).



Las otras respuestas son bastante exhaustivas y Closures in Ruby cubre ampliamente las diferencias funcionales. Tenía curiosidad sobre qué método funcionaría mejor para los métodos que aceptaban un bloque, así que escribí algunos puntos de referencia (saliendo de esta publicación de Paul Mucur ). Comparé tres métodos:

  • & bloquear en la firma del método
  • Usando &Proc.new
  • yield embalaje en otro bloque

Aquí está el código:

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

El rendimiento fue similar entre Ruby 2.0.0p247 y 1.9.3p392. Aquí están los resultados para 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)

Agregar un parámetro explícito &block cuando no siempre se usa realmente ralentiza el método. Si el bloque es opcional, no lo agregue a la firma del método. Y, para pasar bloques, el yield envolver en otro bloque es el más rápido.

Dicho esto, estos son los resultados de un millón de iteraciones, así que no te preocupes demasiado por eso. Si un método hace que su código sea más claro a expensas de una millonésima de segundo, úselo de todos modos.


Por cierto, solo para actualizar esto al día actual usando:

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

En Intel i7 (1.5 años de edad).

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)

Aún 2 veces más lento. Interesante.