Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby 3 JITの最新情報: 現状と今後(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

画像は英語記事からの引用です。

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のせいでこの言い訳が使えなくなってしまいました。今後の言い訳には「AoT設定をデバッグ中」とか「ETLスクリプトを実行中」がおすすめです。この手もまだ使えます。

「プログラマーが合法的におさぼりする言い訳No.1:『コンパイル中です』」
「おらっ!仕事中だぞ」「コンパイル中でーす」「さよか」
うう、JITのせいでこの言い訳が使えなくなってしまいました。
今後の言い訳には「AoT設定をデバッグ中」とか「ETLスクリプトを実行中」が当分おすすめです。

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アプリを実行できるほどには安定していません。ご心配なく、そのときが来れば結果を公表いたします。

これは直近の値ではありません。かつMJITやYARV-MJITの値は今も激しく意味深な変動を繰り返しています。しばらくお待ちください。

これは直近の値ではありません。かつMJITやYARV-MJITの値は今も激しく意味深な変動を繰り返しています。しばらくお待ちください。

CRubyのJITはどこから来たのか

RubyのJITは、しばらく前からある形を取って導入されてきました。JRubyには何年も前からJITがありますし、RubiniusにもしばらくJITがありましたがその後取り除かれました。しかし「純粋な」CRubyにJITが組み込まれたことはかつてありませんでした。その代わりさまざまな実験用ブランチとしてJITが姿を表しましたが、Rubyリリースに取り入れられたことはなかったのです。

Shyouhei Urabeによる"deoptimization"ブランチはかなりよかったのですが、大きな成功には至りませんでした。このJITは非常に純粋かつ非常にシンプルであり、可能な最適化はごくわずかにとどまりましたが、その代わり余分なメモリをほとんど必要としないことが保証されていました。Rubyコアチームはメモリ使用量にとても気を遣っています。

その後、最近になってVladimir MakarovRuby 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へ移行したときの方法よりずっと好ましいと思います。

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までお知らせください。よろしくお願いします。

関連記事

Ruby: mallocでマルチスレッドプログラムのメモリが倍増する理由(翻訳)

メモリを意識したRubyプログラミング(翻訳)

Rubyのヒープをビジュアル表示する(翻訳)

Rubyのメモリ割り当て方法とcopy-on-writeの限界(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。