Ruby: Proc # call против yield - PullRequest
       22

Ruby: Proc # call против yield

73 голосов
/ 11 сентября 2009

Каковы поведенческие различия между следующими двумя реализациями в Ruby метода 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" }

К «поведенческим различиям» относятся обработка ошибок, производительность, поддержка инструментов и т. Д.

Ответы [ 6 ]

50 голосов
/ 11 сентября 2009

Я думаю, что первый на самом деле является синтаксическим сахаром другого. Другими словами, нет никаких поведенческих различий.

Что позволяет вторая форма, так это «сохранить» блок в переменной. Тогда блок может быть вызван в другой момент времени - обратный вызов.


Ok. На этот раз я пошел и сделал быстрый тест:

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

Результаты интересны:

      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)

Это показывает, что использование block.call почти в 2 раза медленнее, чем использование yield .

9 голосов
/ 16 июля 2013

Вот обновление для Ruby 2.x

ruby ​​2.0.0p247 (2013-06-27 редакция 41674) [x86_64-darwin12.3.0]

Мне надоело писать тесты вручную, поэтому я создал небольшой модуль бегуна под названием 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

Выход

                      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)

Я думаю, что самое удивительное здесь то, что bench_yield медленнее, чем bench_proc. Я бы хотел немного больше понять, почему это происходит.

6 голосов
/ 11 сентября 2009

Они выдают разные сообщения об ошибках, если вы забыли передать блок:

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

Но они ведут себя одинаково, если вы пытаетесь передать «нормальный» (не блочный) аргумент:

> 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'
5 голосов
/ 20 сентября 2013

Другие ответы довольно полны, и Замыкания в Ruby подробно охватывают функциональные различия. Мне было любопытно, какой метод лучше всего подойдет для методов, которые , необязательно принимают блок, поэтому я написал несколько тестов (выход из этого сообщения Пола Мукура ). Я сравнил три метода:

  • & блок в сигнатуре метода
  • Использование &Proc.new
  • Упаковка yield в другой блок

Вот код:

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

Производительность была одинаковой между Ruby 2.0.0p247 и 1.9.3p392. Вот результаты для 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)

Добавление явного параметра &block, когда он используется не всегда, действительно замедляет метод. Если блок является необязательным, не добавляйте его в сигнатуру метода. И, для передачи блоков вокруг, упаковка yield в другом блоке является самой быстрой.

Тем не менее, это результаты для миллиона итераций, так что не беспокойтесь об этом слишком сильно. Если один метод делает ваш код более понятным за счет миллионной доли секунды, все равно используйте его.

2 голосов
/ 15 ноября 2016

Я обнаружил, что результаты различаются в зависимости от того, вы заставляете Ruby создавать блок или нет (например, ранее существовавший процесс).

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

Дает результаты:

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

Если вы измените do_call(&existing_block) на do_call{}, вы обнаружите, что в обоих случаях он примерно в 5 раз медленнее. Я думаю, что причина этого должна быть очевидна (потому что Ruby вынужден создавать Proc для каждого вызова).

0 голосов
/ 04 мая 2011

Кстати, просто чтобы обновить это до текущего дня, используя:

ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux]

На Intel i7 (1,5 года).

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)

Еще в 2 раза медленнее. Интересно.

...