thrice
メソッドのRuby=)の次の2つの実装の動作の違いは何ですか?
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" }
「動作の違い」には、エラー処理、パフォーマンス、ツールサポートなどが含まれます。
最初のものは実際にはもう一方の構文糖衣だと思います。つまり、行動の違いはありません。
ただし、2番目の形式で許可されるのは、変数にブロックを「保存」することです。その後、他の時点でブロックを呼び出すことができます-コールバック。
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の使用がyieldの使用よりもほぼ2倍遅いことを示しています。
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
より遅いことです。これがなぜ起こっているのか、もう少し理解していただけたらと思います。
他の答えはかなり徹底しており、 Rubyのクロージャー は機能の違いを広範囲にわたってカバーしています。 オプションでブロックを受け入れるメソッドにどのメソッドが最適かを知りたいので、ベンチマークを作成しました( this Paul Mucur post )。 3つの方法を比較しました。
&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
を別のブロックにラップするのが最も高速です。
ただし、これらは100万回の反復の結果であるため、あまり心配する必要はありません。 1つのメソッドが100万分の1秒のコストでコードを明確にする場合は、とにかくそれを使用します。
ブロックを渡すのを忘れると、異なるエラーメッセージが表示されます。
> 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'
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を構築することを強制されるため)。
ところで、これを現在の日に更新するには、次を使用します。
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倍遅い。面白い。