web-dev-qa-db-fra.com

Ruby: Proc # call vs yield

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.

74
Sam Stokes

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.

50
jpastuszek

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.

9
user633183

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:

  • bloquer dans la signature de la méthode
  • Utilisation de &Proc.new
  • Enveloppement yield dans un autre bloc

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

6
cbrauchli

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

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

2
ioquatix

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.

0
Travis Reeder