Ruby: ProС# call vs yield

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

Под "поведенческими различиями" я включаю обработку ошибок, производительность, поддержку инструмента и т.д.

Ответ 1

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

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


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.

Ответ 3

Здесь обновление для 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. Хотелось бы, чтобы у меня было немного больше понимания того, почему это происходит.

Ответ 4

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

> 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

Другие ответы довольно подробные и Closures in 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 в другом блоке выполняется быстрее.

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

Ответ 6

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

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 для каждого вызова).

Ответ 7

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

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)

Все еще 2x медленнее. Интересно.