Rubyのメソッド呼び出しの比較
ふと思い付きでRubyのメソッド呼び出しのやり方で速度がどう変わるのかベンチマークをとってみた。
Benchmark.ips do |x|
x.report 'direct' do |times|
i = 0
while i < times; i += 1
obj.to_s
end
end
x.report '__send__' do |times|
i = 0
while i < times; i += 1
obj.__send__(:to_s)
end
end
# ...略...
x.compare!
end
これまでbenchamrk-ipsの使い方をきちんと調べていなかったのに気付いた。ブロック引数のことを知らず、むだに自力でループをまわしていたよ。(もちろんベンチマークの対象として明示的なループを含める場合もある。)
ともあれ上の抜粋のような感じで、比較対象を以下とした。
- 直接呼び出し
__send__
public_send
Method#call
Proc#call
eval
で定義したメソッドの直接呼び出しeval
で定義した特異メソッドの直接呼び出しdefine_method
で定義したメソッドの直接呼び出しdefine_singleton_method
で定義したメソッドの直接呼び出し
前提として次のような場面で使われる書き方を想定している(つもり)。
def process!(type)
# typeに応じた処理に振り分ける
end
Ruby 2.4.2での結果はこのようなものだった:
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin17]
Warming up --------------------------------------
direct 420.953k i/100ms
__send__ 385.947k i/100ms
public_send 366.926k i/100ms
Method#call 391.803k i/100ms
Proc#call 362.438k i/100ms
Runner#run 404.075k i/100ms
SRunner#run 401.759k i/100ms
define_method 385.629k i/100ms
define_s_method 384.551k i/100ms
Calculating -------------------------------------
direct 39.934M (± 1.5%) i/s - 199.953M in 5.008397s
__send__ 23.080M (± 1.0%) i/s - 115.398M in 5.000515s
public_send 18.459M (± 1.8%) i/s - 92.465M in 5.011171s
Method#call 24.425M (± 0.8%) i/s - 122.243M in 5.005233s
Proc#call 15.354M (± 0.6%) i/s - 76.837M in 5.004481s
Runner#run 26.642M (± 0.9%) i/s - 133.345M in 5.005392s
SRunner#run 27.387M (± 0.8%) i/s - 137.000M in 5.002728s
define_method 23.140M (± 0.7%) i/s - 116.074M in 5.016350s
define_s_method 22.547M (± 2.5%) i/s - 112.673M in 5.001530s
Comparison:
direct: 39933553.7 i/s
SRunner#run: 27386819.7 i/s - 1.46x slower
Runner#run: 26642207.3 i/s - 1.50x slower
Method#call: 24424736.8 i/s - 1.63x slower
define_method: 23140233.0 i/s - 1.73x slower
__send__: 23079618.5 i/s - 1.73x slower
define_s_method: 22547456.3 i/s - 1.77x slower
public_send: 18459211.6 i/s - 2.16x slower
Proc#call: 15354192.9 i/s - 2.60x slower
Method#call
がいいところにいるのが少し意外だった。
ちなみにRuby 2.3.5、2.2.8では以下の通り。2.2→2.4で並び順が違っている点、比較の幅が小さくなっている点、など興味深い。
ruby 2.3.5p376 (2017-09-14 revision 59905) [x86_64-darwin16]
[...]
Comparison:
direct: 41255655.5 i/s
SRunner#run: 26035506.5 i/s - 1.58x slower
Runner#run: 25723329.6 i/s - 1.60x slower
__send__: 23379461.9 i/s - 1.76x slower
Method#call: 21672615.9 i/s - 1.90x slower
public_send: 18619443.0 i/s - 2.22x slower
Proc#call: 15689293.4 i/s - 2.63x slower
define_method: 13979854.7 i/s - 2.95x slower
define_s_method: 13939945.4 i/s - 2.96x slower
ruby 2.2.8p477 (2017-09-14 revision 59906) [x86_64-darwin16]
[...]
Comparison:
direct: 39551645.0 i/s
Runner#run: 22201895.0 i/s - 1.78x slower
SRunner#run: 22007592.6 i/s - 1.80x slower
Method#call: 20553863.6 i/s - 1.92x slower
__send__: 19987987.2 i/s - 1.98x slower
public_send: 19081953.9 i/s - 2.07x slower
Proc#call: 13147615.6 i/s - 3.01x slower
define_method: 11609181.7 i/s - 3.41x slower
define_s_method: 11604994.9 i/s - 3.41x slower
eval
系が上位にくるのは当然ではあるものの、その準備には相応にコストがかかるはずである。そのあたりも含めたベンチマークをとってみよう、と思ったのだが、ここで対象としているような呼び出されるメソッドがかなり軽量なケースでは、かなりとんでもない回数のメソッド呼び出しがある状況でなければもとが取れないという結果になった。
まあ、それはそうか。実際のコードで検証しなければならないだろう。
ただ、PassengerやUnicornなどのように予めプロセスを待期させておくケースなど、運用上、初期化コストを無視できることもある。また、プロセスがそれなりに長時間生き続けるケースでも同様のことが言える。
参考までにRuby 2.4.2での結果を一部抜粋しておく:
Comparison:
direct x100: 108983.2 i/s
public_send x100: 83613.7 i/s - 1.30x slower
Method#call x100: 71422.0 i/s - 1.53x slower
Proc#call x100: 63886.4 i/s - 1.71x slower
define_s_method x100: 38516.4 i/s - 2.83x slower
define_method x100: 33664.1 i/s - 3.24x slower
SRunner#run x100: 14121.4 i/s - 7.72x slower
Runner#run x100: 12891.5 i/s - 8.45x slower
Comparison:
direct x1000: 10918.1 i/s
public_send x1000: 8458.4 i/s - 1.29x slower
Method#call x1000: 8377.2 i/s - 1.30x slower
Proc#call x1000: 7198.3 i/s - 1.52x slower
define_s_method x1000: 6876.8 i/s - 1.59x slower
define_method x1000: 6660.9 i/s - 1.64x slower
SRunner#run x1000: 5311.9 i/s - 2.06x slower
Runner#run x1000: 5148.2 i/s - 2.12x slower
Comparison:
direct x10000: 1099.5 i/s
Method#call x10000: 848.1 i/s - 1.30x slower
public_send x10000: 845.9 i/s - 1.30x slower
define_s_method x10000: 747.6 i/s - 1.47x slower
SRunner#run x10000: 746.4 i/s - 1.47x slower
define_method x10000: 741.0 i/s - 1.48x slower
Runner#run x10000: 737.3 i/s - 1.49x slower
Proc#call x10000: 728.2 i/s - 1.51x slower
Comparison:
direct x100000: 110.5 i/s
Method#call x100000: 85.7 i/s - 1.29x slower
public_send x100000: 84.6 i/s - 1.31x slower
SRunner#run x100000: 78.6 i/s - 1.41x slower
Runner#run x100000: 78.3 i/s - 1.41x slower
define_s_method x100000: 76.0 i/s - 1.45x slower
define_method x100000: 75.9 i/s - 1.46x slower
Proc#call x100000: 73.4 i/s - 1.51x slower
Comparison:
direct x400000: 27.5 i/s
Method#call x400000: 21.3 i/s - 1.29x slower
public_send x400000: 21.2 i/s - 1.29x slower
SRunner#run x400000: 19.9 i/s - 1.38x slower
Runner#run x400000: 19.7 i/s - 1.39x slower
define_s_method x400000: 19.1 i/s - 1.44x slower
define_method x400000: 19.0 i/s - 1.45x slower
Proc#call x400000: 18.2 i/s - 1.51x slower
x10000
というのは、1回の初期化と10,000回のメソッド呼び出しをしたことを表している。
ベンチマークコードはこんなの。