How JRuby Makes Ruby Fast を読んで、試したログ

(20090423 補足追加)

How JRuby Makes Ruby Fast – Charles Oliver Nutter – Java, Ruby, and JVM guy trying to make sense of it allベンチマークを実行してみた。解釈間違い等あれば、ご指摘ください。ベンチコードは参照元のまま、以下の通り。いろんな最適化レベルでパフォーマンスはかるには格好のものらしい。

def tak x, y, z
  if y >= x
    return z
  else
    return tak( tak(x-1, y, z),
                tak(y-1, z, x),
                tak(z-1, x, y))
  end
end

require 'benchmark'

N = (ARGV.shift || 1).to_i

Benchmark.bm do |make|
  N.times do
    make.report do
      i = 0
      while i<10
        tak(24, 16, 8)
        i += 1
      end
    end
  end
end

で、まずRuby1.8.7と1.9、JRubyインタプリタモードで比較

zsh% ruby -v bench_tak.rb 5
ruby 1.8.7 (2009-04-08 patchlevel 160) [i686-darwin9.6.0]
      user     system      total        real
 17.330000   0.100000  17.430000 ( 17.878270)
 17.320000   0.100000  17.420000 ( 17.865022)
 17.370000   0.100000  17.470000 ( 17.916406)
 17.370000   0.090000  17.460000 ( 17.923585)
 17.380000   0.120000  17.500000 ( 18.020999)

zsh% ~/r191/ruby -v bench_tak.rb 5
ruby 1.9.1p0 (2009-01-30 revision 21907) [i386-darwin9.6.0]
      user     system      total        real
  3.500000   0.020000   3.520000 (  3.610300)
  3.490000   0.020000   3.510000 (  3.600984)
  3.490000   0.020000   3.510000 (  3.590130)
  3.490000   0.020000   3.510000 (  3.625073)
  3.490000   0.020000   3.510000 (  3.626134)

zsh% jruby -v -X-C bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
 19.569000   0.000000  19.569000 ( 19.503000)
 20.894000   0.000000  20.894000 ( 20.894000)
 21.127000   0.000000  21.127000 ( 21.128000)
 21.126000   0.000000  21.126000 ( 21.126000)
 21.180000   0.000000  21.180000 ( 21.180000)

「-X-C」オプションは「disable all compilation」、インタプリタモード、かつ「client VM」で実行した結果。
次は、「Server VM」で実行してみる

zsh% jruby --server -v -X-C bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  7.832000   0.000000   7.832000 (  7.776000)
  8.012000   0.000000   8.012000 (  8.012000)
  8.187000   0.000000   8.187000 (  8.187000)
  8.193000   0.000000   8.193000 (  8.193000)
  8.201000   0.000000   8.201000 (  8.201000)

インタプリタモードなのでJVMバイトコードコンパイルされていない
(追記:2009/04/23 start)

ソフトウェアの実行時にコードのコンパイルを行い実行速度の向上を図る方式である。通常のコンパイラソースコード(あるいは中間コード)から対象CPUの機械語への変換を実行前に(コンパイル時に)行う。通常のコンパイラソースコード(あるいは中間コード)から対象CPUの機械語への変換を実行前に(コンパイル時に)行う。これをJITと対比して事前コンパイル (Ahead Of Time, AOT) コンパイルと呼ぶ。

インタプリタ方式と比較すると性能面では以下のような差が出てくる

* 機械語に変換されるため、コンパイル後の実行速度はインタプリタ方式の数倍の性能となる
* モジュールやクラス、関数のロード時にコンパイルが行われるため、プログラムの起動には時間がかかる
* 一度コンパイルしたコードを保持するために、より多くのメモリ容量を必要とする

実行時コンパイラ - Wikipedia
(追記:2009/04/23 end)
なので、次は「Server VM」で、「コンパイルモード」で実行してみる。ただし最適化は行われていない
(追記:2009/04/23 start)
Server VM と Client VM の違い

The "server" VM differs from the default "client" VM in that it will optimistically inline code across calls and optimize the resulting code as a single unit. This obviously allows it to eliminate costly x86 CALL operations, but even more than that it allows optimizing algorithms which span multiple calls.

The difference is the way that it uses the Just-in-Time compilers.

The client mode uses a JIT compiler (originally from Apple), which basically compiles the java bytecodes into native machine code as soon as it is loaded.

The server mode uses a JIT compile from Sun, which uses the Java interpreter on the java bytecodes, for a while. Once enough data has been collected about the java class, it is then JIT compiled to native machine code, using the profiling data that it has collected to optimize the compilation.

Henry

JVM Server Mode vs Client Mode (Java in General forum at Coderanch)
Client VM はコードがロードされた際に逐一コンパイルが行われる。Server VM は呼び出すコードもまとめてコンパイルされる。また複数回の実行により最適化される。長時間存続するアプリケーションに向いている。
(追記:2009/04/23 end)

zsh% jruby --server -v -J-Djruby.astInspector.enabled=false bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  4.889000   0.000000   4.889000 (  4.834000)
  3.572000   0.000000   3.572000 (  3.572000)
  3.554000   0.000000   3.554000 (  3.554000)
  3.558000   0.000000   3.558000 (  3.558000)
  3.551000   0.000000   3.551000 (  3.551000)

astはAbstract Structure Tree:抽象構文木のことだろう。最適化されていないとはどういうことかというと、JustInTimeでネイティブコードに変換しているかららしい。(del 2009/04/23)
(追記:2009/04/23 start)

Because we're now JITing Ruby code as JVM bytecode, and the JVM eventually JITs JVM bytecode to native code, our Ruby code actually starts to benefit from the JVM's built-in optimizations.

RubyのコードはJVM bytecodeにJITコンパイルされ、JVM機械語に変換する。上記の実行は、その流れでJVMのbuilt-inな最適化の恩恵を得始めた初期段階。最低限の最適化ということみたい。
(追記:2009/04/23 end)
次は、デフォルトの最適化レベル「heap scope elimination」での実行。局所変数をインメモリではなく、外部のコンテキストから参照されない場合はJava局所変数としてコンパイルする。またそれらはCPUのレジスタに格納されるとのことらしい。

zsh% jruby --server -v bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  3.431000   0.000000   3.431000 (  3.372000)
  2.347000   0.000000   2.347000 (  2.348000)
  2.334000   0.000000   2.334000 (  2.334000)
  2.358000   0.000000   2.358000 (  2.358000)
  2.345000   0.000000   2.345000 (  2.345000)

ちなみにオプションなしの場合より、少し早い

zsh% jruby -v bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  3.646000   0.000000   3.646000 (  3.575000)
  2.578000   0.000000   2.578000 (  2.578000)
  2.575000   0.000000   2.575000 (  2.575000)
  2.566000   0.000000   2.566000 (  2.567000)
  2.581000   0.000000   2.581000 (  2.581000)

さて、次は「backtrace-only frames」。このへんから疲れてきた。
(追記:2009/04/23 start)
ただ、

This is where we start to break Ruby a bit, though there are ways around it.

なので、副作用に気をつけはじめなくてはならない
(追記:2009/04/23 start)

zsh% jruby --server -v -J-Djruby.compile.frameless=true bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  3.121000   0.000000   3.121000 (  3.064000)
  2.199000   0.000000   2.199000 (  2.199000)
  2.205000   0.000000   2.205000 (  2.205000)
  2.203000   0.000000   2.203000 (  2.202000)
  2.174000   0.000000   2.174000 (  2.174000)

次も同じオプションだが、

You'll notice the command line here is the same; that's because we're venturing into more and more experimental code, and in this case I've actually forced "frameless" to be "no heap frame" instead of "backtrace-only heap frame".

とのこと。だが「frame」がどういうものか分かってないので・・・
(追記:2009/04/23 start)
frameはJVMのスタック領域のこと?
(追記:2009/04/23 end)

zsh% jruby --server -v -J-Djruby.compile.frameless=true bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  3.117000   0.000000   3.117000 (  3.060000)
  2.151000   0.000000   2.151000 (  2.151000)
  2.144000   0.000000   2.144000 (  2.144000)
  2.144000   0.000000   2.144000 (  2.144000)
  2.154000   0.000000   2.154000 (  2.154000)

つぎは、「some optimizations for math operators」

zsh% jruby --server -v -J-Djruby.compile.frameless=true -J-Djruby.compile.fastops=true bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  2.830000   0.000000   2.830000 (  2.763000)
  1.881000   0.000000   1.881000 (  1.882000)
  1.870000   0.000000   1.870000 (  1.871000)
  1.867000   0.000000   1.867000 (  1.867000)
  1.870000   0.000000   1.870000 (  1.870000)

So JRuby has experimental "fast math" operations to turn most Fixnum math operators into static calls rather than dynamic ones, allowing most math operations to inline directly into the caller.

なので、

This version of "fast ops" makes it impossible to override Fixnum#+ and friends, since whenever we call + on a Fixnum it's going straight to the code.

という副作用があるよ、らしい。んー、だんだん引用ばかりになってきた。
次は、各スレッドで他のスレッドが生きているかチェックする機能をオフにする模様

zsh% jruby --server -v -J-Djruby.compile.frameless=true -J-Djruby.compile.fastops=true -J-Djruby.compile.positionless=true -J-Djruby.compile.threadless=true bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  2.777000   0.000000   2.777000 (  2.721000)
  1.808000   0.000000   1.808000 (  1.808000)
  1.813000   0.000000   1.813000 (  1.813000)
  1.823000   0.000000   1.823000 (  1.823000)
  1.814000   0.000000   1.814000 (  1.814000)

次は、これまでの実験的な最適化をまとめたもの?またそれらの最適化がsafetyなものかを調査する。また動的な呼び出しをインライン化する試みでもあるらしい?分かってないなー、おれ。

zsh% jruby --server -v --fast bench_tak.rb 5
jruby 1.3.0 (ruby 1.8.6p287) (2009-04-21 r6586) (Java HotSpot(TM) 64-Bit Server VM 1.6.0_07) [x86_64-java]
      user     system      total        real
  2.143000   0.000000   2.143000 (  2.076000)
  1.087000   0.000000   1.087000 (  1.087000)
  1.104000   0.000000   1.104000 (  1.104000)
  1.098000   0.000000   1.098000 (  1.098000)
  1.096000   0.000000   1.096000 (  1.096000)
zsh%

・最適化することによる副作用分かっていない
・frameの概念わかってない
・コードのインライン化とはどういうことか分かってない
・まだ続きがあるが、とりあえず今日はここまで
・ひきつづき調べたい