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.
Ответ 2
Поведенческая разница между различными типами рубиновых замыканий была широко документирована
Ответ 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 медленнее. Интересно.