概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Ruby 3 and JIT: Where, When and How Fast?
- 原文公開日: 2017/12/26
- 著者: Noah Gibbs
- サイト: Appfolio Engineering
画像は英語記事からの引用です。
Ruby 3 JITの最新情報: 現状と今後(翻訳)
Ruby 3にJITが導入されるというニュースは既にお聞きおよびかと思います。JITはどこからやってくるのでしょうか?いつ導入されるのでしょうか?それでどれだけ高速化されるのでしょうか?JITがあるとデバッグが心配な場合にオフにできるのでしょうか?
そもそもJITって何?
JITは「just-in-time」の略ですが、日常会話の「間に合ったー!」「滑り込みセーフ」の意味でなければ、特に実行時コンパイラ(Just-In-Time Compiler)を指します。現在のRubyはコードを逐次解釈して実行するインタプリタですが、JITを用いると、Rubyプログラムの一部をマシン語に変換して、Unixコマンドやexeファイルのように実行します。特に、JITは私たちが日頃読んでいるRubyコードを、プロセッサに合わせて最も自然かつ高速なコード(しばしば機械語やマシン語("machine code" or "machine language")などと呼ばれます)に変換します。
JITは「普通の」コンパイラといくつかの点で異なっています。最大の違いは、プログラム全体をコンパイルするわけではない点です。その代わり、最も頻繁に実行される部分だけをコンパイルし、そのプログラムでの使われ方にうまく合わせて最速で実行できるようにコンパイルします。JITは、プログラムでメソッドがどのように呼ばれているかについて推測する必要はありません。プログラムをしばらく監視して情報をメモし、それからコンパイルを開始します。
JITでどれだけ速くなるか
この世には「嘘」、「ひどい嘘」、そして「ベンチマーク」があります(訳注: 元ネタ)。JITによる高速化を正確な値で表すなど無理な相談です。正確な値など存在しないからです。しかし、特定のプログラムについては、完全に合理的な負荷をかけた状態で50%、150%、あるいは250%までJITで高速化できるケースが多々あります。現実的な負荷の元で、500%以上もの高速化を達成するケースすらいくつかあります。ただし、JITよりインタプリタの方が速いケースも若干あることは言うまでもありません。なぜなら、現実世界には常に最適化されているものなど存在しないからです。
現時点での無難かつシンプルなCRuby向けJIT実装では、およそ30%〜50%のパフォーマンス向上が見られ、測定方法次第では最大150%に達することもあります。30%〜50%はJITにしてはかなり控えめな値ですが、これらのブランチはまだまだシンプルですし、30%〜50%という値は過小評価のしようがありません。これは3年分から10年分の「通常」リリースに相当するスピードアップであり、わずか1〜2年のJITへの取り組みでこれほどの成果を達成したのです。そして通常のスピードアップに加えて、こうした大きなスピードアップは現在も起き続けています。さらにJITは長期に渡って改良を重ねることができます。JITは、昔ながらの「純粋なインタプリタ」のRubyでは到底不可能だった最適化の世界へと大きく扉を開いているのです。それこそが、JITを搭載したRuby実装が既に劇的な高速化の可能性を秘めている理由です。TruffleRubyなどの実装は著しいメモリオーバーヘッドを伴いますが、コードを900%あるいはそれ以上スピードアップできます。この戦略はCRubyには合いませんが、それでも高速化は確かに可能なのです。
私は、「どんだけ速くなる?」という質問にはたいていRails Ruby Benchの結果を添えて回答しています(結局私の作ったgemではありますが)。しかし現時点のMJITは、大規模な並列実行を行うRailsアプリを実行できるほどには安定していません。ご心配なく、そのときが来れば結果を公表いたします。
CRubyのJITはどこから来たのか
RubyのJITは、しばらく前からある形を取って導入されてきました。JRubyには何年も前からJITがありますし、RubiniusにもしばらくJITがありましたがその後取り除かれました。しかし「純粋な」CRubyにJITが組み込まれたことはかつてありませんでした。その代わりさまざまな実験用ブランチとしてJITが姿を表しましたが、Rubyリリースに取り入れられたことはなかったのです。
Shyouhei Urabeによる"deoptimization"ブランチはかなりよかったのですが、大きな成功には至りませんでした。このJITは非常に純粋かつ非常にシンプルであり、可能な最適化はごくわずかにとどまりましたが、その代わり余分なメモリをほとんど必要としないことが保証されていました。Rubyコアチームはメモリ使用量にとても気を遣っています。
その後、最近になってVladimir Makarov(Ruby 2.4のハッシュテーブルを再構築したあのMakarovです)が、メモリを食わない強力なJIT実装「MJIT」を作りました。MJITは既存のCコンパイラ(GCCやCLang)をRuby JIT向けに強化します。MakarovがMJITの動作を解説するキーノートスピーチのためにRubyKaigiに招かれたのも、MJITへの期待の大きさゆえです。Makarovは最初にRubyを改造して、スタックベースのVMではなくレジスタベースのVMを用いるようにし、その基礎の上にJITを構築しています。しかしMJITはまだ歴史が浅く、一般的にリリースできるほどには安定していません。どんなRubyプログラムでも動かせる、クラッシュしないRubyを作るのは困難であり、MJITはまだその段階にありません。しかし最近の結果によるとMJITのCPUベンチマークはRuby 2.0.0の230%にも達しているので、大筋では間違っていないことは確かです。
MJITの登場とちょうど同じ頃、Takashi KokubunはEvan Phoenixの初期の成果にヒントを得てLLVMベースの強力なRuby JIT実装「LLRB」を作っていました。LLRBもまた、MJITと同様、Ruby世界を支えるほどには洗練されていませんでしたが、TakashiはMJITから多くの成果を取り入れて開発を続け、YARV-MJITに結実しました。
YARV-MJITは、レジスタベースVMとなるためにMJITの変更点を取り入れました。この変更を反映すれば、Rubyはさらに高速になる代わりに、すべてを安定させるために必要なテストも増やさなければなりません。変更をやめておけば、Ruby JITの機能が少なくなる代わりにリリースを早められます。皆が望んでいるのは、できるだけ小規模な機能かつできるだけ早期のリリースであることを覚えていますか?YARV-MJITはまさにそれを実践する原則です。「JITを単に追加してみてはどうか」「さほど高速にならなくても構わないからJITを追加してみてはどうか」「JITをデフォルトでオフにして、欲しい人だけがこの新しい実験的機能を使うということにしてはどうか」という具合にです。しかしこのJITは、一部の機能をオフにしたMJITと同じものです。
JITはいつ使えるのか
言うまでもなくこれは難しい質問です。どんな問題が見つかり、どれだけ修正が容易かでリリース時期は変わってくるでしょう。
YARV-MJITのissue #1782が現在オープンになっているので、Rubyへの導入は秒読み状態なのかもしれませんが、Ruby 2.5.0のクリスマスリリースに含まれることはありません。そしてそれがベストです。
YARV-MJITもMJITも着々と改良を重ねています。VladはMJITが本当に成熟するには1年ほどかかるだろうと見込んでいます。しかしYARV-MJITによって通常のRubyリリースに取り込まれるJITが完璧な状態である必要はありません。JITを有効にするよう指定すればオンになります。
狭い意味ではいつリリースされてもおかしくありません。しかし(JITが)デフォルトでオンの状態でリリースされるにはおそらく1年、またはそれ以上はかかるでしょう。イミュータブルな文字列のときと同様、Rubyはさらに多くの新機能をオプトインとして取り込んでいます。これはFeature Toggles(Feature FlagsやFeature Flippersとも呼ばれます)に近い方法で、新機能が完全な状態でなくてもインクルードできますが、その場合は新機能同士が衝突しないことの確認が必要です。私はこのアプローチについて、Ruby 1.8から1.9へ移行したときの方法よりずっと好ましいと思います。
Link: Merge MJIT infrastructure with conservative JIT compiler by k0kubun · Pull Request #1782 · ruby/ruby: https://t.co/8rR6r5bmcS
— Yukihiro Matz (@yukihiro_matz) December 26, 2017
JITの導入をいち早く知る方法/JITをオフにする方法
YARV-MJITがいつRubyに導入されるかを知りたい方は、上のプルリク#1782を追いかけることをおすすめします。
JITで何か問題が起きるのではと心配な方は、JITは自由にオンオフできることを覚えておきましょう。RUBYOPT
環境変数は、MJITやYARV-MJITを含む含まないにかかわらず任意のCRubyで使えますし、Rubyを実行するたびに(Rubyコード入力中でなくても)コマンドライン引数で渡すこともできます。
現時点では、JITはYARV-MJITでもデフォルトでオフになっています。オンにするには以下のオプションを指定します。
export RUBYOPT="-j"
YARV-MJITでは、JITパラメータを渡さないようにするだけでJITを無効にできます。つまり、-j
を最初に渡さなければJITは動きません。
JITを動かすためのオプションは-j
の他にもあります。たとえば、-j:w
を渡すとJITのwarningがすべて出力され、-j:s
を渡すとJITが作成する.cソースファイルを削除せずに/tmpディレクトリに保存します。
JITでもっと遊んでみたい方は、MJITまたはYARV-MJITが有効になっているRubyでruby --help
を実行することをおすすめします。ただしこれらのオプションは、YARV-MJITがRubyに取り込まれる前に変更される可能性があるので、ローカルの(Ruby)バージョンをチェックするべきです。
MJIT options:
s, save-temps Save MJIT temporary files in /tmp
c, cc C compiler to generate native code (gcc, clang, cl)
w, warnings Enable printing MJIT warnings
d, debug Enable MJIT debugging (very slow)
v=num, verbose=num
Print MJIT logs of level num or less to stderr
n=num, num-cache=num
Maximum number of JIT codes in a cache
JITを支援する方法/Rubyの次のJITについて
RubyのJITを使ってみたいのであれば、試しに動かしてみるのが簡単かつ手っ取り早いでしょう。
cloneとビルドは以下の方法で行えます。
cd ~/my_src_dir
git clone git@github.com:k0kubun/yarv-mjit.git
cd yarv-mjit
autoconf
./configure
make check
ビルドが終われば、テストやインストールをローカルで行えるようになります。私は以下のrunruby
スクリプトでローカルテストするのが好みです。
cd ~/my_src_dir/yarv-mjit
./tool/runruby.rb ~/my_src_dir/my_ruby_script.rb
rvm
を使えば、ローカルでビルドしたRubyインタプリタをマウントできます。
# コンパイルの後で実行すること!
rvm mount ~/my_src_dir/yarv-mjit yarv-mjit
rvm use ext-yarv-mjit
-j
でJITがオンになり、-j:w
でwarningがオンになることをお忘れなく。コードをYARV-MJITで実行してみた方は、ぜひお知らせください。Twitterでお知らせいただくと助かりますが、他の方法でも構いません。
JITで何か問題が生じたら、小規模な再現手順に切り分けてから、YARV-MJITのRuby bug #14235までお知らせください。よろしくお願いします。