Ruby: Proc # Call gegen Yield

77

Was sind die Verhaltensunterschiede zwischen den folgenden beiden Implementierungen der thriceMethode 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.

Sam Stokes
quelle
Der Verhaltensunterschied zwischen verschiedenen Arten von
Rubinverschlüssen
8
Randnotiz: def thrice(&block)ist selbstdokumentierender, insbesondere im Vergleich zu einem yieldirgendwo in einer großen Methode vergrabenen.
Nathan Long

Antworten:

51

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 .

jpastuszek
quelle
8
Ich denke, Ruby wäre konsistenter, wenn das wahr wäre (dh wenn yieldes nur syntaktischer Zucker wäre Proc#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/… ), dass yieldes effizienter ist, da es nicht zuerst ein ProcObjekt erstellen und dann seine callMethode aufrufen muss .
Sam Stokes
Erneutes Update mit Benchmarks: Ja, ich habe auch einige Benchmarks durchgeführt und Proc#callwar mehr als yielddoppelt 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: Er Proc#callwar ungefähr 8x so langsam wie yield. Das heißt, yieldauf JRuby war doppelt so schnell wie yieldauf MRT.
Sam Stokes
Ich habe meine auf MRI 1.8.7p174 x86_64-linux gemacht.
jpastuszek
3
Sie vermissen auch einen dritten Fall : def test(&block) ; 10.times(&block) ; end, der genauso getestet werden sollte wie der Ertragsfall.
Rampion
1
Die obigen Benchmarks entsprechen ebenfalls ungefähr Ruby v2.1.2. block.callist ~ 1,7x langsamer als yield.
Gav
9

Hier ist ein Update für Ruby 2.x.

ruby 2.0.0p247 (27.06.2013, Revision 41674) [x86_64-darwin12.3.0]

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

                      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)

Ich denke, das Überraschendste hier ist, dass bench_yieldes langsamer ist als bench_proc. Ich wünschte, ich hätte ein bisschen mehr Verständnis dafür, warum dies geschieht.

Vielen Dank
quelle
2
Ich glaube, das liegt daran, dass bench_procder unäre Operator den Prozess tatsächlich in den Block des timesAufrufs verwandelt und den Aufwand für die Blockerstellung für das timesIn bench_yieldund überspringt bench_call. Dies ist eine seltsame Art der Verwendung von Sonderfällen, die in den yieldmeisten 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 &)
Matt Sanders
Integer#timesAufrufe yield(die c-Version, rb_yield, die einen Wert annimmt, der einen Block darstellt). Deshalb ist bank_proc so schnell.
Nate Symer
6

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'
Sam Stokes
quelle
6

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:

  • & Methodensignatur blockieren
  • Verwenden von &Proc.new
  • yieldIn einen anderen Block einwickeln

Hier 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 &blockParameters, 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 Einwickeln yieldin 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.

cbrauchli
quelle
2

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), werden do_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).

ioquatix
quelle
0

Ü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.

Travis Reeder
quelle