【Ruby】Ruby2.6.0をコンパイルし、ベンチマークを取ってみる

2018年12月27日Mac,Ruby

こんにちは、しきゆらです。

毎年、この時期はRubyの最新版リリースが熱いですよね。

ということで、早速環境を作っていきましょう。


Rubyは、Ruby3x3という目標のもと、高速化のための改良が続けられています。

今回は、高速化のための変更としてJITコンパイラが導入されました。

オプションとして–jitをつけると、JITを使って動作するようです。

詳しい更新内容については、以下を参照して下さい。

高速化以外の変更でよく使いそうなものとしては、「終端なしRange」の追加でしょうかね。

あとは、何気にCSVファイルの扱いも高速になっているようです。

さて、それでは実際にコンパイルしてみて、どのくらい早くなったのかを、簡単なベンチマークを取りながらみてみましょう。


コンパイルしてみる

こちらは、いつも通りで何も問題なくできました

# ソースコードの取得
wget https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.0.tar.gz
tar zxvf ruby-2.6.0.tar.gz
cd ruby-2.6.0

# いつものコンパイル
./configure --prefix=/usr/local/Ruby --with-openssl-dir=/usr/local/opt/openssl
make
sudo make install

サクッと終わらせて、ベンチマークを取ってみましょう。

ベンチマークことはじめ

標準ライブラリを使ったベンチマークの書き方

Rubyでベンチマークを取るのは、意外と簡単。

標準添付ライブラリに、その名も「benchmark」という物が入っています。

これを使えば、簡単に記録を取ることができます

コードの書き方は、以下のようなに書くことができます。

require 'benchmark'
 
Benchmark.bm(10) do |r|
  r.report 'label' do
    # ベンチマークを取るコード
  end
end

複数コードのベンチマークを撮りたい場合は、中身のr.report部分を増やせばいいだけ。

bm(10)の数字は、表示するラベルの文字数です。

ラベルの長さに応じて調整してください。

コード自体は簡単ですね。


なお、このベンチマークは処理時間を計測するので、値が小さいほど高速で処理できていることを表します。

benchmark-ipsを使った書き方

このGemでベンチマークすると「処理回数(イテレーション回数)/秒」を計測してくれます。

https://qiita.com/itkrt2y/items/d34a593078f5b99d5fbe


標準添付ライブラリでは、処理にかかった時間だけを表示していました。

一方で、このGemを使うと「処理回数」を計測してくれます。

決まった時間の中で何回処理できるか、を計測してくれるため値が高いほど高速であることを表しています。

# 上記サイトより引用
require 'benchmark/ips'

Benchmark.ips do |x|
  x.report("addition") { 1 + 2 }
  x.report("addition2") do |times|
    i = 0
    while i < times
      1 + 2
      i += 1
    end
  end
  x.report("addition3", "1 + 2")
  x.report("addition-test-long-label") { 1 + 2 }

  x.compare!
end


ベンチマークを取る

では、これまで利用していた2.5.3と、最新の2.6.0、そして、JITを利用した場合の3パターンの比較をしてみます。

比較するのは、以下の3場面です

  • HashにランダムなkeyとvalueをもつHashをマージする
  • CSVファイルを読み込み、一部を取り出して計算
  • 海外サイトに上がっていたJITコンパイラ向けのベンチマークコード

今回使うコードは以下の通りです。

なお、今回使用したCSVファイルは、国勢調査のデータセットを利用しました。

require 'benchmark/ips'
require 'csv'

repeat_no = 100_000
charlist = ['a'..'z', 'A'..'Z'].map(&:to_a).flatten

def calculate(a, b, n = 40_000_000)
  i = 0
  c = 0
  while i < n
    a = a * 16_807 % 2_147_483_647
    b = b * 48_271 % 2_147_483_647

    c += 1 if (a & 0xffff) == (b & 0xffff)
    i += 1
  end
  c
end

Benchmark.ips do |r|
  r.iterations = 3
  r.report 'hash' do
    hash = {}
    repeat_no.times do
      tmp = {
        charlist[0..rand(charlist.length)].join => rand(10_000)
      }
      hash.merge!(tmp)
    end
  end

  r.report 'csv load' do
    csv = CSV.read('./c01.csv').map { |row| [row[1], row[2], row[6]] }.transpose
    csv[2][1..-1].inject(0) { |num, item| num += item.to_i } / csv[2].length
  end

  r.report('calculate') do |_times|
    calculate(65, 8921, 100_000)
  end
end

コードはipsになっていますが、実行時間の方とipsの両方を同じコードでとっています。

ベンチマークの結果

結果は、以下の通りでした。

まずは、Ruby2.5.3。

5回計測して、hashはだいたい0.3秒ほど
それ以外は、0.01秒ほどでした。

次に、Ruby2.6.0。

2.5.3と比べて、3つどれも早くなっているようです。

最後は、Ruby2.6.0 JITオプションあり。

結果は、JIT無しよりも遅いくらい。

処理時間では大きな変化は見られませんでしたが、確実に2.6.0では速度が出ているようです。

では、次はipsの方を見てみます。

Ruby2.5.3。

hashは5秒間に17回ほど実行できているようです。

Ruby2.6.0。

2.5.3よりも、処理回数が上がっていますね。
特に、calculateの伸びはすごいです。
約651 -> 約1.7kと大きく伸びています。

Ruby2.6.0 JIT。

処理時間と同じような結果。
しかし、calculateではぐんと伸びています。

結果を見てみると、Ruby2.6.0は全体として2.5.3よりも少し早く動作するようですね。

JITは、普段はあまり意識しなくてもいいような感じですが、特定の場合に大きくスピードが上がるようです。

まとめ

今回は、Ruby2.6.0のリリースに合わせて、前バージョンからどのくらい処理が早くなったのかを見てみました。

正直、今回使ったコードがあまりよくないのか、大きな差は見られませんでした。

それでも、2.6.0は前バージョンよりも高速なことがわかりました。

また、JITは特定の場合に威力を発揮することがわかりました

今回は、ここまで。

おわり

Posted by しきゆら