Ruby: Proc # appel vs rendement

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, le support 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 la deuxième forme permet cependant, c’est de “sauver” le bloc dans une variable. Ensuite, le bloc peut être appelé à un autre moment – rappel.


    D’accord. Cette fois-ci, je suis allé faire un rapide benchmark:

     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 deux fois plus lente que l’utilisation du rendement .

    La différence de comportement entre les différents types de fermetures de rbuy a été largement documentée

    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 repères manuellement alors j’ai créé un petit module de coureur 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 

    Sortie

      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 est que bench_yield est plus lent que bench_proc . Je souhaite avoir un peu plus de compréhension de la raison pour laquelle cela se produit.

    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-block):

     > 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' 

    Les autres réponses sont assez complètes et Closures in Ruby couvre largement les différences fonctionnelles. J’étais curieux de savoir quelle méthode fonctionnerait le mieux pour les méthodes qui acceptent éventuellement un bloc, alors j’ai écrit quelques tests (en sortant de ce post de Paul Mucur ). J’ai comparé trois méthodes:

    • & bloquer la signature de la méthode
    • Utiliser &Proc.new
    • yield emballage 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 réellement la méthode. Si le bloc est facultatif, ne l’ajoutez pas à la signature de la méthode. Et, pour le passage des blocs, le yield emballage dans un autre bloc est le plus rapide.

    Cela dit, ce sont les résultats pour un million d’itérations, alors ne vous inquiétez pas trop. Si une méthode rend votre code plus clair au désortingment d’un millionième de seconde, utilisez-le quand même.

    J’ai trouvé que les résultats étaient différents selon que vous obligiez Ruby à construire le bloc ou non (par exemple, un processus 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 = Ssortingng.new while (i -= 1) > 0 do_call(&existing_block) end end x.report("yield with block") do |i| buffer = Ssortingng.new while (i -= 1) > 0 do_yield(&existing_block) end end x.report("yield") do |i| buffer = Ssortingng.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.502ki/100ms yield with block 269.487ki/100ms yield 262.597ki/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 modifiez 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 devrait être évidente (parce que Ruby est obligé de construire un Proc pour chaque invocation).

    BTW, juste pour mettre à jour cette journée 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) 

    Encore 2x plus lent. Intéressant.