Quelles sont les différences de comportement entre les deux implémentations suivantes dans Ruby de la méthode 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" }
Par "différences de comportement", j'inclus la gestion des erreurs, les performances, la prise en charge des outils, etc.
Je pense que le premier est en fait un sucre syntaxique de l'autre. En d'autres termes, il n'y a pas de différence de comportement.
Ce que le second formulaire permet cependant, c'est de "sauvegarder" le bloc dans une variable. Ensuite, le bloc peut être appelé à un autre moment - rappel.
D'accord. Cette fois, j'y suis allé et j'ai fait un test rapide:
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
Les résultats sont intéressants:
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)
Cela montre que l'utilisation de block.call est presque 2x plus lente que l'utilisation de yield.
Voici une mise à jour pour Ruby 2.x
Ruby 2.0.0p247 (2013-06-27 révision 41674) [x86_64-darwin12.3.0]
J'en ai eu marre d'écrire des benchmarks manuellement alors j'ai créé un petit module runner appelé 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
Production
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)
Je pense que la chose la plus surprenante ici est que bench_yield
est plus lent que bench_proc
. Je souhaite avoir eu un peu plus de compréhension pour pourquoi cela se produit.
Les autres réponses sont assez approfondies et fermetures en Ruby couvre largement les différences fonctionnelles. J'étais curieux de savoir quelle méthode serait la plus performante pour les méthodes qui facultativement acceptent un bloc, j'ai donc écrit quelques repères (en partant ce Paul Mucur post ). J'ai comparé trois méthodes:
&Proc.new
yield
dans un autre blocVoici le code:
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
Les performances étaient similaires entre Ruby 2.0.0p247 et 1.9.3p392. Voici les résultats pour 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'ajout d'un paramètre explicite &block
Lorsqu'il n'est pas toujours utilisé ralentit vraiment la méthode. Si le bloc est facultatif, ne l'ajoutez pas à la signature de la méthode. Et, pour passer des blocs autour, envelopper yield
dans un autre bloc est le plus rapide.
Cela dit, ce sont les résultats pour un million d'itérations, alors ne vous en faites pas trop. Si une méthode rend votre code plus clair au détriment d'un millionième de seconde, utilisez-la quand même.
Ils donnent des messages d'erreur différents si vous oubliez de passer un bloc:
> 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'
Mais ils se comportent de la même manière si vous essayez de passer un argument "normal" (non bloquant):
> 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'
J'ai trouvé que les résultats sont différents selon que vous forcez Ruby pour construire le bloc ou non (par exemple un proc préexistant).
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
Donne les résultats:
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 vous changez do_call(&existing_block)
en do_call{}
, Vous constaterez qu'il est environ 5 fois plus lent dans les deux cas. Je pense que la raison de cela devrait être évidente (car Ruby est obligé de construire un Proc pour chaque invocation).
BTW, juste pour mettre à jour ce jour en utilisant:
Ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux]
Sur Intel i7 (1,5 ans).
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)
Toujours 2x plus lent. Intéressant.