更新情報

こんにちは、トランスネットの泉です。

だいぶ日が経ちましたが、先日開催されましたRubyKaigi2014で“The Ruby Challenge”というプレゼンテーションがありました。

The Ruby Challenge: https://therubychallenge.com/

「画面に並んだ二つのコードのうち、より速い方を当ててTシャツを貰おう!」といった内容で、会場は大いに盛り上がり非常に印象深いプレゼンテーションのひとつでした。

さて、実際の開発においても「どちらの書き方の方が速いのだろう…」といった局面にあたることは多々あります。
「どちらが速いか」確実に確認する方法、それは言うまでも無く測ることです。

ということでRubyコラムDig Ruby! 第2回目は、Rubyで書かれたコードの実行速度を計測する方法の基本中の基本、”Benchmark”ライブラリをご紹介します。

library benchmark

benchmarkライブラリは、Ruby言語に標準添付されている「ベンチマークを取るためのライブラリです。」
特にGemをインストールしたりすることもなく、requireするだけで利用することができます。

require “benchmark”

 

Benchmark.measure

実行時間の計測には、measureメソッドを利用します。

与えられたブロックを実行して、経過した時間を Process.#times で計り、 Benchmark::Tms オブジェクトを生成して返します。

Ruby 2.1.0 リファレンスマニュアル> ライブラリ一覧> benchmarkライブラリ> Benchmarkモジュール

 

requirebenchmark
r = Benchmark.measure do
str =
100_000.times do
str += @
end
end
puts r
view rawdig_ruby_2-1.rb hosted with ❤ by GitHub

$ ruby dig_ruby_2-1.rb
0.850000   0.770000   1.620000 (  1.623218)

ブロック内の処理に掛かった時間が出力されます。

それぞれの値が何を示したものか確認するためには、定数CAPTIONをあわせて出力します。

require benchmark
r = Benchmark.measure do
str =
100_000.times do
str += @
end
end
puts Benchmark::CAPTION
puts r
view rawdig_ruby_2-2.rb hosted with ❤ by GitHub

$ ruby dig_ruby_2-2.rb
user     system      total        real
0.870000   0.790000   1.660000 (  1.659533)

 

Benchmark.bm

複数の計測結果を一度に表示して比較することができる、bmメソッドも用意されています。

Benchmark.#benchmark メソッドの引数を簡略化したものです。

Ruby 2.1.0 リファレンスマニュアル > ライブラリ一覧 > benchmarkライブラリ > Benchmarkモジュール bm

Benchmark::Report オブジェクトを生成し、それを引数として与えられたブロックを実行します。

Ruby 2.1.0 リファレンスマニュアル > ライブラリ一覧 > benchmarkライブラリ > Benchmarkモジュール benchmark

説明だけではなかなかピンと来ません。
是非、リファレンスのサンプルコードを実行してみてください。

計測、比較してみる

先日、こんなコードを見掛けました。

foo.instance_eval(bar)

barにはStringとしてfooインスタンスのメソッド名が入っています。
同じような形でメソッドを呼び出すためのメソッドとしては、sendメソッドがあります。

foo.send(bar)

文字列を「評価」するinstance_evalに比べるとsendの方が速そうですが、本当にそうでしょうか。
実際に計測して比較してみましょう。
今回は、メソッドを比較するための簡易的なクラスを用意しました。
折角ですので、sendの引数を変えてみるなど他にも色々試してみます。

require benchmark
class Person
attr_accessor :name
def initialize(args)
self.name = args[:name]
end
end
alice = Person.new(name: Alice)
bob = Person.new(name: Bob)
Benchmark.bm(15) do |x|
x.report(normal) { alice.name; bob.name; }
x.report(send(symbol)) { alice.send(:name); bob.send(:name) }
x.report(send(string)) { alice.send(name); bob.send(name) }
x.report(send(str.to_sym)) { alice.send(name.to_sym)
bob.send(name.to_sym) }
x.report(instance_eval) { alice.instance_eval(name)
bob.instance_eval(name) }
x.report(eval) { eval(alice.name); eval(bob.name) }
end
view rawdig_ruby_2-3.rb hosted with ❤ by GitHub

$ ruby dig_ruby_2-3.rb
user     system      total        real
normal            0.000000   0.000000   0.000000 (  0.000006)
send(symbol)      0.000000   0.000000   0.000000 (  0.000005)
send(string)      0.000000   0.000000   0.000000 (  0.000004)
send(str.to_sym)  0.000000   0.000000   0.000000 (  0.000014)
instance_eval     0.000000   0.000000   0.000000 (  0.000033)
eval              0.000000   0.000000   0.000000 (  0.000027)

…一瞬で終わってしまいました。結果も誤差のような範囲です。

このような場合は、同じ処理を繰り返し実行して比較します。
今回は5百万回(!)指定してみました。
小さな回数では、上記のように一瞬で終わってしまい比較することは困難です。
とはいえ、余り大きな回数を指定してしまいますと、中々結果が返ってこない、マシンに負荷を掛けすぎてしまう、など不具合があります。
このあたりは、色々と計測しているうちに勘所を掴めるようになるはずです。

それでは、実行してみます。
サンプルコードの動作確認は実施していますが、試される場合は自己責任でお願いいたします。

require benchmark
class Person
attr_accessor :name
def initialize(args)
self.name = args[:name]
end
end
alice = Person.new(name: Alice)
bob = Person.new(name: Bob)
Benchmark.bm(15) do |x|
x.report(normal) { 5_000_000.times{ alice.name; bob.name; } }
x.report(send(symbol)) { 5_000_000.times{ alice.send(:name); bob.send(:name) } }
x.report(send(string)) { 5_000_000.times{ alice.send(name); bob.send(name) } }
x.report(send(str.to_sym)) { 5_000_000.times{ alice.send(name.to_sym)
bob.send(name.to_sym) } }
x.report(instance_eval) { 5_000_000.times{ alice.instance_eval(name)
bob.instance_eval(name) } }
x.report(eval) { 5_000_000.times{ eval(alice.name); eval(bob.name) } }
end

$ ruby dig_ruby_2-4.rb
user     system      total        real
normal            0.450000   0.000000   0.450000 (  0.453427)
send(symbol)      0.770000   0.000000   0.770000 (  0.773540)
send(string)      2.280000   0.000000   2.280000 (  2.278333)
send(str.to_sym)  2.300000   0.000000   2.300000 (  2.301340)
instance_eval    61.750000   0.210000  61.960000 ( 61.984973)
eval             67.380000   0.180000  67.560000 ( 67.572950)

今回ははっきりと差が現れました。
予想通りinstance_evalよりもsendの方が速いようですが、その差はなんと約30倍でした。
また、sendの引数に設定するメソッド名は、symbolと文字列では約3倍の差があることもわかりました。
今後の参考になりそうです。

これで「速さ」に確信を持てましたので、安心してコードレビューで指摘することができます:-)

まとめ

Rubyコードの実行時間を計測するためのライブラリ、Benchmarkのもっとも基本的な使い方をご紹介しました。
途中でご覧いただいたように、1回コードを実行するだけの時間は一瞬かもしれません。
しかし、プログラムはこれら一瞬の積み重ねで構築されています。
「神は細部に宿る」という言葉もあります。
一行一行全てのコードに気を配って改善できるようなエンジニアを目指したいものです。

それでは今日はこの辺で。次回をお楽しみに♪

※なお、今回の実行環境は以下の通りです:

  • MacBook Pro (Retina, Mid 2012)
  • 2.3GHz Intel Core i7, 8GB RAM
  • OS X Yosemite 10.10
  • ruby 2.1.3p242 (2014-09-19 revision 47630) [x86_64-darwin13.0]