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).
La diferencia de comportamiento entre los diferentes tipos de cierres de rubí ha sido ampliamente documentada
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.