Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyコードのベンチマーク:パフォーマンス測定と実装比較の基本

Rubyには標準ライブラリとしてベンチマークツールが付属しており、コードのパフォーマンスを手軽に測定できます。特に、2つの実装を比較してどちらが速いかを調べたい場合に威力を発揮します。

この記事では、文字列をキーとするハッシュ({"foo" => "bar"} のようなもの)を、シンボルをキーとするハッシュ({:foo => "bar"} のようなもの)に変換するタスクを例に取り上げます。例を通して、英字アルファベットの各文字をキーと値のペアにしたハッシュを使用します。

ハッシュを手入力せずに素早く生成するために、文字の範囲をテスト用ハッシュに変換します。後で使えるよう、input 変数に格納しておきましょう。

input = ("a".."z").map {|letter| [letter, letter]}.to_h
# => {"a"=>"a", "b"=>"b", "c"=>"c", ... , "z"=>"z"}

テスト用の input 変数が用意できたので、まず1つの実装を書いて、そのパフォーマンスを確認してみます。入力ハッシュのすべてのキーを文字列からシンボルに変換する、すっきりしたワンライナーは次のようになります。

input.map { |key, value| [key.to_sym, value] }.to_h
# => {:a=>"a", :b=>"b", :c=>"c", ... , :z=>"z"}

この実装では、map メソッドを使ってハッシュをループし、各キーと値のペアに対してブロックを実行します。ブロック内ではキーをシンボルに変換し、新しく作成したシンボルキーと元の値からなる2要素の配列を返します。

map の結果は、26個のキーと値のペア(配列)を含む配列になります。最終的にはハッシュが必要なので、#to_h を使ってこの配列をハッシュに変換し直しています。

Benchmark.measure で計測する

動作する実装ができたところで、RubyのBenchmarkモジュールを使ってパフォーマンスを測定してみましょう。

require 'benchmark'
 
input = ('a'..'z').map { |letter| [letter, letter] }.to_h
 
puts Benchmark.measure {
  50_000.times do
    input.map { |key, value| [key.to_sym, value] }.to_h
  end
}

Benchmark.measure はブロックを受け取り、その実行時間を記録しながらブロックを実行します。戻り値はレポート文字列で、puts を使ってコンソールに出力できます。

このコードは非常に短いため、目に見える結果を得るために50,000回実行しています。

$ ruby bench.rb
  0.810000   0.000000   0.810000 (  0.816964)

レポート文字列には4つの数値が表示されます。それぞれ、ユーザーCPU時間(コードの実行に費やされた時間)、システムCPU時間(カーネル内の処理に費やされた時間)、両者の合計、そして括弧内に実際の経過時間(ウォールクロック時間)を表します。

ウォールクロック時間を見ると、上記のコードブロックを50,000回実行するのに約800ミリ秒かかっていることがわかります。これはなかなか良い数字に見えますが、別の実装と比較しない限り、それが速いのか遅いのかは判断できません。

Benchmark.bm で複数の実装を比較する

Benchmark.measure のほかに、Rubyは Benchmark.bm も提供しています。こちらは複数のコードサンプルを実行し、その結果を一覧で出力できます。各サンプルに対して、ラベル名と実行するブロックを指定して Benchmark#report を呼び出します。

require 'benchmark'
 
input = ("a".."z").map { |letter| [letter, letter] }.to_h
n = 50_000
 
Benchmark.bm do |benchmark|
  benchmark.report("Hash[]") do
    n.times do
      input.map { |key, value| [key.to_sym, value] }.to_h
    end
  end
 
  benchmark.report("{}.tap") do
    n.times do
      {}.tap do |new_hash|
        input.each do |key, value|
          new_hash[key.to_sym] = value
        end
      end
    end
  end
end

このベンチマークでは、Benchmark.bm を使って2つの実装をそれぞれ50,000回実行して比較します。最初の測定ブロックは、先ほどの例と同じものです。

2番目の測定ブロックでは、やや長めの実装を使用しています。この実装は最初に新しい空のハッシュを作成し、文字列キーのハッシュをループしながら、各要素を新しいハッシュに追加していきます。この方法なら、ハッシュを一旦配列に変換して、最後にまたハッシュへ戻すという二度手間が発生しません。

ベンチマークを再度実行すると、コードは長く(そして先ほどのワンライナーより少し地味ではありますが)、この実装の方が25%以上速いことがわかります。

$ ruby bench.rb
       user     system      total        real
Hash[]  0.850000   0.000000   0.850000 (  0.851106)
{}.tap  0.610000   0.020000   0.630000 (  0.637070)

さらなるベンチマーク活用のヒント

コードベースの重要な部分に取り組む際には、ベンチマークを実行して異なる実装を比較することで、実行速度に関するより深い洞察が得られます。複数の実装を比較してパフォーマンスへの影響を理解することで、アンチパターンを避け、より高速なRubyコードを書けるようになるでしょう。

ヒント:一般的なコーディングイディオムの多くはすでにベンチマークが取られており、その結果は「fast-ruby」として公開されています。事前に例を読み通しておけば、将来のベンチマーク作業の手間を省けるかもしれません。

この例には他にも試せるオプションがあり、Rubyのベンチマークライブラリにはさらに高度な機能も多数用意されていますが、この記事でRubyにおけるベンチマークの仕組みの入門としては十分でしょう。ベンチマークについてさらに詳しく知りたい場合や、質問・提案がある場合は、@AppSignalまでお気軽にお知らせください。

  1. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端

  2. Rubyでの静的分析入門!parser gemでメソッド定義を抽出する方法

    ソースコードを解析して、すべてのメソッドがどこで定義され、どんな引数を受け取るのかを把握したいと思ったことはありませんか? どうすれば実現できるのでしょうか? 最初に思いつくのは、正規表現(regexp)を書くことかもしれません。 しかし、もっと良い方法があるとしたらどうでしょう? 答えは「あります」! 静的解析(Static Analysis)とは、ソースコードそのものから情報を抽出するためのテクニックです。 これは、ソースコードをトークンへと変換する(パースする)ことで実現されます。 それでは早速見ていきましょう! parser gemを使う Rubyには標準ライブラリとしてRipper