Was sind die Verhaltensunterschiede zwischen den folgenden beiden Implementierungen der thrice
Methode in Ruby ?
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" }
Unter "Verhaltensunterschieden" verstehe ich Fehlerbehandlung, Leistung, Werkzeugunterstützung usw.
def thrice(&block)
ist selbstdokumentierender, insbesondere im Vergleich zu einemyield
irgendwo in einer großen Methode vergrabenen.Antworten:
Ich denke, der erste ist tatsächlich ein syntaktischer Zucker des anderen. Mit anderen Worten, es gibt keinen Verhaltensunterschied.
Was die zweite Form erlaubt, ist das "Speichern" des Blocks in einer Variablen. Dann kann der Block zu einem anderen Zeitpunkt aufgerufen werden - Rückruf.
In Ordnung. Diesmal habe ich einen schnellen Benchmark durchgeführt:
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
Die Ergebnisse sind interessant:
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)
Dies zeigt , dass die Verwendung von block.call ist fast 2x langsamer als die Verwendung Ausbeute .
quelle
yield
es nur syntaktischer Zucker wäreProc#call
), aber ich denke nicht, dass es wahr ist. zB gibt es das unterschiedliche Fehlerbehandlungsverhalten (siehe meine Antwort unten). Ich habe auch gesehen, dass es vorgeschlagen wurde (z. B. stackoverflow.com/questions/764134/… ), dassyield
es effizienter ist, da es nicht zuerst einProc
Objekt erstellen und dann seinecall
Methode aufrufen muss .Proc#call
war mehr alsyield
doppelt so langsam wie bei MRI 1.8.6p114. Auf JRuby (1.3.0, JVM 1.6.0_16 Server VM) war der Unterschied noch auffälliger: ErProc#call
war ungefähr 8x so langsam wieyield
. Das heißt,yield
auf JRuby war doppelt so schnell wieyield
auf MRT.def test(&block) ; 10.times(&block) ; end
, der genauso getestet werden sollte wie der Ertragsfall.block.call
ist ~ 1,7x langsamer alsyield
.Hier ist ein Update für Ruby 2.x.
Ich hatte es satt , Benchmarks manuell zu schreiben, also habe ich ein kleines Runner-Modul namens Benchable erstellt
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
Ausgabe
Ich denke, das Überraschendste hier ist, dass
bench_yield
es langsamer ist alsbench_proc
. Ich wünschte, ich hätte ein bisschen mehr Verständnis dafür, warum dies geschieht.quelle
bench_proc
der unäre Operator den Prozess tatsächlich in den Block destimes
Aufrufs verwandelt und den Aufwand für die Blockerstellung für dastimes
Inbench_yield
und überspringtbench_call
. Dies ist eine seltsame Art der Verwendung von Sonderfällen, die in denyield
meisten Fällen immer noch schneller zu sein scheint. Weitere Informationen zu Proc to Block Assignment: ablogaboutcode.com/2012/01/04/the-ampersand-operator-in-ruby (Abschnitt: The Unary &)Integer#times
Aufrufeyield
(die c-Version, rb_yield, die einen Wert annimmt, der einen Block darstellt). Deshalb ist bank_proc so schnell.Sie geben unterschiedliche Fehlermeldungen aus, wenn Sie vergessen, einen Block zu übergeben:
> 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'
Sie verhalten sich jedoch gleich, wenn Sie versuchen, ein "normales" (nicht blockiertes) Argument zu übergeben:
> 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'
quelle
Die anderen Antworten sind ziemlich gründlich und Closures in Ruby behandelt ausführlich die funktionalen Unterschiede. Ich war gespannt, welche Methode für Methoden, die optional einen Block akzeptieren , am besten geeignet ist, und schrieb daher einige Benchmarks (in diesem Beitrag von Paul Mucur ). Ich habe drei Methoden verglichen:
&Proc.new
yield
In einen anderen Block einwickelnHier ist der 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
Die Leistung war zwischen Ruby 2.0.0p247 und 1.9.3p392 ähnlich. Hier sind die Ergebnisse für 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)
Das Hinzufügen eines expliziten
&block
Parameters, wenn dieser nicht immer verwendet wird, verlangsamt die Methode wirklich. Wenn der Block optional ist, fügen Sie ihn nicht zur Methodensignatur hinzu. Und um Blöcke herumzugeben, ist das Einwickelnyield
in einen anderen Block am schnellsten.Dies sind jedoch die Ergebnisse für eine Million Iterationen. Machen Sie sich also keine allzu großen Sorgen. Wenn eine Methode Ihren Code auf Kosten einer Millionstel Sekunde klarer macht, verwenden Sie sie trotzdem.
quelle
Ich habe festgestellt, dass die Ergebnisse unterschiedlich sind, je nachdem, ob Sie Ruby zwingen, den Block zu erstellen oder nicht (z. B. ein bereits vorhandener Prozess).
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
Gibt die Ergebnisse:
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
Wenn Sie zu wechseln
do_call(&existing_block)
, werdendo_call{}
Sie feststellen, dass es in beiden Fällen etwa 5x langsamer ist. Ich denke, der Grund dafür sollte offensichtlich sein (weil Ruby gezwungen ist, für jeden Aufruf einen Proc zu erstellen).quelle
Übrigens, nur um dies auf den aktuellen Tag zu aktualisieren, indem Sie:
ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux]
Auf Intel i7 (1,5 Jahre alt).
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)
Immer noch 2x langsamer. Interessant.
quelle