Ruby1.9 や Rubinius は MRI と比べて本当に速いのか

Ruby1.9 や Rubinius は MRI と比べて本当に速いのかどうか、確かめてみた。

結論からいうと、fibonacci sequence のような「ベンチマーク向き」のテストでは確かに速くなっている。しかし、文字列操作を多用するような実アプリケーションに近づくほど、速くはならないか、逆に遅くなる。


まず最初のテストスクリプト

test1.rb: (fibonacci sequence)

def f(n)
  if n <= 1
    return 1
  else
    return f(n-1) + f(n-2)
  end
end

puts f(34)


time コマンドで計測してみる。Rubinius は起動が遅いので 0.5 秒くらい割り引いて考える必要があるが、ここでは無視する。

$ time ruby test1.rb
9227465

real    0m27.373s
user    0m27.002s
sys     0m0.071s

$ time ruby1.9 test1.rb
9227465

real    0m5.911s
user    0m5.863s
sys     0m0.023s

$ time rubinius test1.rb
9227465

real    0m10.434s
user    0m10.244s
sys     0m0.130s


これをみると、たしかに Ruby1.9 や Rubinius は Ruby1.8 (MRI) と比べて高速化しているように見える。

しかし、fibonacci sequence が速くなったからといって、実アプリケーションも速くなるとは限らない。そこで、文字列結合を多用する別のベンチマークスクリプトを用意した。このスクリプトは、Web application を意識している。

test2.rb: (string concatenation)

items = [
  { 'id'=>1001, 'name'=>'AAA' },
  { 'id'=>1002, 'name'=>'BBB' },
  { 'id'=>1003, 'name'=>'CCC' },
  { 'id'=>1004, 'name'=>'DDD' },
  { 'id'=>1005, 'name'=>'EEE' },
  { 'id'=>1006, 'name'=>'FFF' },
  { 'id'=>1007, 'name'=>'GGG' },
  { 'id'=>1008, 'name'=>'HHH' },
  { 'id'=>1009, 'name'=>'III' },
  { 'id'=>1010, 'name'=>'JJJ' },
]

n = 100000
result = nil
n.times do
  buf = ''
  buf << <<END
<html>
  <body>
   <table>
     <thead>
       <tr>
         <th>id</th><th>name</th>
       </tr>
     </thead>
     <tbody>
END
  is_odd = false
  for item in items
    is_odd = !is_odd
    buf << "       <tr class=\"#{is_odd ? 'odd' : 'even'}\">
         <td>#{item['id']}</td><td>#{item['name']}</td>
       </tr>
"
  end
  buf << <<END
     </tbody>
   </table>
  </body>
</html>
END
  result = buf
end

#print result


これを実行してみると、Ruby1.8 (MRI) が最も速く、Ruby1.9 (YARV) と Rubinius は遅くなっていることがわかる。特に Rubinius は Ruby1.8 と比べて 3 倍近く遅い。

$ time ruby test2.rb
real    0m8.229s
user    0m8.169s
sys     0m0.027s

$ time ruby1.9 test2.rb
real    0m11.101s
user    0m11.017s
sys     0m0.045s

$ time rubinius test2.rb
real    0m22.160s
user    0m21.774s
sys     0m0.203s


Ruby1.9 が遅いのは、たぶん YARV のせいではなく、String が M18N になったことが関連していると思われる。また Rubinius が遅いのは、String の動作の tuning がまだされていないせい (参考: ticket #359 [TASK] Improve performance of String)。

もうひとつ、Erubis 付属のベンチマークを実行してみた。以下がベンチマーク結果である (ただし必要なとこだけを抜粋)。

$ ruby bench.rb -n 1000
*** ntimes=1000, testmode=execute
                                    user     system      total        real
ERB                             6.670000   0.070000   6.740000 (  6.781463)
ERB(cached)                     2.160000   0.090000   2.250000 (  2.263453)
Erubis::Eruby                   1.950000   0.050000   2.000000 (  2.018663)
Erubis::Eruby(cached)           1.360000   0.080000   1.440000 (  1.444246)
Erubis::TinyEruby               1.720000   0.060000   1.780000 (  1.780438)

$ ruby1.9 bench.rb -n 1000
*** ntimes=1000, testmode=execute
                                    user     system      total        real
ERB                             6.880000   0.130000   7.010000 (  7.035035)
ERB(cached)                     2.860000   0.140000   3.000000 (  3.005437)
Erubis::Eruby                   2.210000   0.100000   2.310000 (  2.326651)
Erubis::Eruby(cached)           1.590000   0.130000   1.720000 (  1.719405)
Erubis::TinyEruby               2.050000   0.100000   2.150000 (  2.157506)

$ rubinius bench.rb -n 1000
*** ntimes=1000, testmode=execute
                                    user     system      total        real
ERB                           189.277537   0.000000 189.277537 (189.277537)
ERB(cached)                   120.888135   0.000000 120.888135 (120.888123)
Erubis::Eruby                  60.735775   0.000000  60.735775 ( 60.735763)
Erubis::Eruby(cached)          59.077564   0.000000  59.077564 ( 59.077554)
Erubis::TinyEruby              63.876757   0.000000  63.876757 ( 63.876746)


絶望した! Rubinius のあまりの遅さに絶望した!

また Ruby1.9 も、Ruby1.8 より遅くなっている。eRuby ではどうしても eval() が絡むからしかたないんだけど、とにかく Ruby1.9 になればなんでもかんでも速くなるというのは間違いであると断言できる。


eval 抜きだとどうなるのか。Erubis のベンチマークスクリプトでは、eval での実行をせず、eRuby ファイルのコンパイルだけを実行することができるので、それで調べてみた。

$ ruby bench.rb -n 1000 -m convert
*** ntimes=1000, testmode=convert
                                    user     system      total        real
ERB                             4.610000   0.060000   4.670000 (  4.733398)
Erubis::Eruby                   0.680000   0.060000   0.740000 (  0.735850)
Erubis::TinyEruby               0.440000   0.050000   0.490000 (  0.491361)

$ ruby1.9 bench.rb -n 1000 -m convert
*** ntimes=1000, testmode=convert
                                    user     system      total        real
ERB                             4.160000   0.100000   4.260000 (  4.291514)
Erubis::Eruby                   0.720000   0.090000   0.810000 (  0.820965)
Erubis::TinyEruby               0.550000   0.080000   0.630000 (  0.635659)

$ rubinius bench.rb -n 1000 -m convert
*** ntimes=1000, testmode=convert
                                    user     system      total        real
ERB                            71.645604   0.000000  71.645604 ( 71.645608)
Erubis::Eruby                   3.654989   0.000000   3.654989 (  3.654977)
Erubis::TinyEruby               3.444701   0.000000   3.444701 (  3.444691)


絶望した! eval ないのにひとケタ遅い Rubinius に絶望した!

また eval がなくても、Ruby1.9 は Ruby1.8 と大して変わらないことがわかる。
結局、Ruby1.9 や Rubinius で速くなるのはバイトコードの実行部分だけであり、String#<< のような built-in method の実行が速くなるわけではない。fibonacchi sequence のベンチマークが速いのは built-in method の呼び出しがない、純粋にバイトコードの実行だけで済むベンチマークだからである。
逆に、String#<< のような built-in method の実行時間が増えれば増えるほど YARV の高速性が生かされないので Ruby1.9 でも速さは変わらなくなるし、Rubinius のように built-in method が遅い場合はベンチマークの結果も Ruby1.8 より遅くなる。


結論: Ruby1.9 にしたからといって速くなるとは限らない。速くなればラッキー、ぐらいに考えていたほうがよさそう。