ようへいの日々精進XP

よかろうもん

(小ネタでごめんネ) Ruby の Benchmark モジュールを使ってみた

この記事は

YAMAP エンジニア Advent Calendar 2022 の第十日目の記事です。

qiita.com

前回

inokara.hateblo.jp

の記事で書いたコードのパフォーマンスを比較してみたくなったので、Benchmark モジュールを使って 2 つのコードのパフォーマンスを計測してみました。(ベンチマークを走らせてみました)

引き続き、以下の環境で検証を行っています。

root@a68e3293a424:/work# ruby -v
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x86_64-linux]

Benchmark モジュール

Benchmark モジュールは、その名の通り Ruby プログラムのベンチマークを取得するモジュールです。

docs.ruby-lang.org

ドキュメントに掲載されているサンプルコードを実行してみます。

require 'benchmark'

n = 50000
Benchmark.benchmark(" "*7 + Benchmark::CAPTION,
                    7,
                    Benchmark::FORMAT,
                    ">total:",
                    ">avg:") do |x|

  tf = x.report("for:")   { for i in 1..n; a = "1"; end }
  tt = x.report("times:") { n.times do   ; a = "1"; end }
  tu = x.report("upto:")  { 1.upto(n) do ; a = "1"; end }

  [tf+tt+tu, (tf+tt+tu)/3]
end

これは、以下の各コードの実行速度を計測し、実行速度の合計と平均も合わせて出力しています。

  • for i in 1..n; a = "1"; end
  • n.times do ; a = "1"; end
  • 1.upto(n) do ; a = "1"; end

実行すると以下のように出力されます。

root@a68e3293a424:/work# ruby sample.rb
                     user     system      total        real
for:      0.011802   0.001178   0.012980 (  0.027640)
times:    0.006102   0.000499   0.006601 (  0.007872)
upto:     0.007856   0.000000   0.007856 (  0.011528)
>total:   0.025760   0.001677   0.027437 (  0.047040)
>avg:     0.008587   0.000559   0.009146 (  0.015680)

出力結果のヘッダ部分 (usersystem 等) は以下のような意味があります。

ヘッダ名 意味
user プログラムによって利用された時間 (Ruby が動いた時間)
system プログラムによって OS が利用された時間
total user + system の時間
real プログラムの実行に掛かったリアルな時間 (プログラムの起動から終了までの実稼働時間)

上記のサンプルコードの実行結果を見ると、times メソッドを利用したコードが最も実行速度が速いようです。

root@a68e3293a424:/work# ruby sample.rb
                     user     system      total        real
for:      0.011802   0.001178   0.012980 (  0.027640)
times:    0.006102   0.000499   0.006601 (  0.007872) ← これが一番速い
upto:     0.007856   0.000000   0.007856 (  0.011528)

尚、前後してしてしまいますが benchmark メソッドの引数は以下の通りです。(ドキュメントより引用させて頂きました。)

引数名 内容
caption レポートの一行目に表示する文字列を指定します。 " "*7 + Benchmark::CAPTION
label_width ラベルの幅を指定します。 7
fmtstr フォーマット文字列を指定します。この引数を省略すると Benchmark::FORMAT が使用されます。 Benchmark::FORMAT
labels ブロックが Benchmark::Tms オブジェクトの配列を返す場合に指定します。 ">total:", ">avg:"

また、benchmark メソッドの引数を省略した bm というメソッドもあります。bm メソッドの場合には、以下のように書くことが出来るとのことです。

require 'benchmark'

n = 50000
Benchmark.bm do |x|
x.report { for i in 1..n; a = "1"; end }
x.report { n.times do   ; a = "1"; end }
x.report { 1.upto(n) do ; a = "1"; end }
end

こちらの方がシンプルで好きです。個人的に。

ベンチマーク

ということで、前回の記事で書いたコードでベンチマークを走らせてみたいと思います。

データが少ないと一瞬で処理が終わってしまう為、対象のデータを多めに約 24 万行分用意しました。

root@a68e3293a424:/work# wc -l 200000.txt
240084 200000.txt

そして、ベンチマーク対象のコードは以下のような感じです。

datas = File.open('200000.txt').read.split("\n")

Benchmark.bm(10) do |x|

  x.report("chunk: ") {
    groups = datas.chunk {|x| x != "" || nil}.map {|x| x.last }
    
    result = []

    groups.each do |g|
      result << g.map(&:to_i).sum
    end
    
    # puts result.max
  }

  x.report("no chunk: ") {
    datas.push("")
    _result = []
    _array = []
    
    datas.each do |d|
      if d.empty?
        _result << _array.sum
        _array = []
      else
        _array << d.to_i
      end
    end
    # puts _result.max
  }
end

ベンチマークのコードを実行すると、以下のような結果となりました。

root@142f31c43c0a:/work# ruby test.rb
                 user     system      total        real
chunk:       0.136952   0.009732   0.146684 (  0.157515)
no chunk:    0.064187   0.000000   0.064187 (  0.072388)
root@142f31c43c0a:/work# ruby test.rb
                 user     system      total        real
chunk:       0.132214   0.008650   0.140864 (  0.151078)
no chunk:    0.059786   0.002620   0.062406 (  0.071507)
root@142f31c43c0a:/work# ruby test.rb
                 user     system      total        real
chunk:       0.136413   0.008141   0.144554 (  0.187495)
no chunk:    0.065624   0.000606   0.066230 (  0.092425)

Enumerable#chunk を使った方が速いのかなーと思っていましたが、実際にはそうでもなさそうです。chunk を使わない方が半分くらいの処理時間となっています。

ちょっと気になった (chunk の部分は速いけど result << g.map(&:to_i).sum に時間が掛かっているのかなーと思った) ので、純粋に chunk メソッドが実行されている部分だけでベンチマークを走らせてみました。

datas = File.open('200000.txt').read.split("\n")

Benchmark.bm(10) do |x|
  x.report("chunk: ") {
    datas.chunk {|x| x != "" || nil}.map {|x| x.last }
  }
end

以下のような結果となりました。

root@142f31c43c0a:/work# ruby test.rb
                 user     system      total        real
chunk:       0.089565   0.012890   0.102455 (  0.104683)
root@142f31c43c0a:/work# ruby test.rb
                 user     system      total        real
chunk:       0.102780   0.014299   0.117079 (  0.146592)
root@142f31c43c0a:/work# ruby test.rb
                 user     system      total        real
chunk:       0.084478   0.010283   0.094761 (  0.098349)

70% くらい chunk の実行に時間を要しているという興味深い結果となりました。

ここから

chunk メソッドがどのように実装されているのかを追うことが出来ると良いのですが、私のスキルでは限界がありますので、Benchmark モジュールの使い方を覚えたところで、今回はここらで筆を置こうと思います。

現場からは以上です。