Ruby 2.6のMJITとは?仕組みとパフォーマンスを徹底解説
Rubyのパフォーマンスは、バージョンが上がるごとに大きく向上してきました。そしてRuby開発チームは、さらに速いRubyを実現するために全力を尽くしています。
その取り組みのひとつが「3×3プロジェクト」です。
その目標とは?
Ruby 3.0を、Ruby 2.0の3倍速くすることです。
このプロジェクトの一環として誕生したのが、新しいMJITコンパイラです。これが本記事のテーマとなります。
MJITとは何か
MJITは「Method Based Just-in-Time Compiler(メソッドベースのジャストインタイムコンパイラ)」の略称です。
これは具体的にどういう意味でしょうか?
Rubyはまず、あなたのコードをYARV命令にコンパイルし、その命令をRuby仮想マシン(VM)が実行します。
JITは、この仕組みにもう1層を追加します。
頻繁に実行される命令を、ネイティブのバイナリコードへコンパイルするのです。
その結果として最適化されたバイナリが生成され、コードがより高速に動作するようになります。
MJITの仕組み
MJITの動作を実際に観察しながら、理解を深めていきましょう。
Ruby 2.6では、--jitオプションを付けて起動するだけでJITを有効化できます。
実行方法は次のとおりです:
ruby --jit app.rb
Ruby 2.6には、JITの挙動を詳細に調べるための専用オプション群が用意されています。ruby --helpを実行すれば一覧を確認できます。
主なオプションは以下のとおりです:
- --jit-wait
- --jit-verbose
- --jit-save-temps
- --jit-max-cache
- --jit-min-calls
まず試すのに適しているのは、verboseオプションでしょう。
あわせて--jit-waitも使います。これは、JITコードのコンパイルが完了するまでRubyを待機させるオプションです。
通常の動作では、JITはワーカースレッド上でコンパイルを行うため、完了を待たずに処理を進めます。
検証用のコマンドはこちらです:
ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "4.times { 123 }"
出力結果:
Successful MJIT finish
うーん、あまり面白い結果ではありませんね。
JITは何もしていません。
なぜでしょうか?
理由は、デフォルト設定ではメソッドが5回以上呼び出されたときにしかJITが動作しないからです(jit-min-callsの初期値)。
では、呼び出し回数を増やして実行してみましょう:
ruby --disable-gems --jit --jit-verbose=1 --jit-wait -e "5.times { 123 }"
今度は興味深い出力が得られます:
JIT success (32.1ms): block in <main>@-e:1 -> /tmp/_ruby_mjit_p13921u0.c
この出力は何を意味しているのでしょうか?
ブロックが5回呼び出されたことで、JITがそれをコンパイルしたのです。このログから読み取れる情報は以下のとおりです:
- コンパイルにかかった時間(
32.1ms) - コンパイルされた対象(
block in <main>) - コンパイル元として生成されたファイル(
/tmp/_ruby_mjit_p13921u0.c)
このファイルはC言語のソースコードで、まずオブジェクトファイル(.o)にコンパイルされ、その後共有ライブラリファイル(.so)へと変換されます。
--jit-save-tempsオプションを付けると、これらの中間ファイルにアクセスできるようになります。
実際の例がこちらです:

以上を踏まえた、JITの動作フローの全体像がこちらです:
- メソッドの呼び出し回数をカウントする
- あるメソッドが5回呼び出されると(
jit-min-callsのデフォルト値)、JITが発火する - そのメソッドの命令を含むCファイルが生成される(YARV命令をインライン化したもの)
- コンパイルは(
--jit-waitを指定しない限り)バックグラウンドで、GCCなどの一般的なCコンパイラを使って行われる - コンパイルが完了すると、以降そのメソッドが呼び出される際に、生成された共有ライブラリが使用される
それでは、この仕組みが実際にどれほど有効なのかを見ていきましょう。
ベンチマークで検証:本当に速くなるのか?
MJITの目的は、Rubyを速くすることです。
では、現時点での成果はどの程度なのでしょうか?
実際に測定して確かめてみましょう!
まずはマイクロベンチマークからです:
| ベンチマーク | 結果(JITなしのRuby 2.6との比較) |
|---|---|
| whileループ | 8倍速い |
| 文字列連結を伴うwhileループ | 10%速い |
| 乗算を伴うwhileループ(Integer) | 4倍速い |
| 乗算を伴うwhileループ(Bignum) | 20%遅い |
| 文字列のupcase | 10%速い |
| 文字列のmatch | 2%遅い |
| 文字列のmatch? | 10%速い |
| 1万個の乱数を含む配列操作 | 20%速い |
結果はまちまちですが、ここからひとつの傾向を読み取ることができます…
MJITはループ処理が非常に得意なようです!
では、より複雑な実アプリケーションではどうなるでしょうか?
シンプルなSinatraアプリで試してみます:
require 'sinatra' get '/' do "apples, oranges & bananas" end
小さなコードに見えますが、このわずかな処理を実行するだけで500以上の異なるメソッドが呼び出されます。JITにとっては十分な仕事量といえるでしょう!
なお、検証環境はSinatra 2.0.4とThin 1.7.2です。
ベンチマークは次のコマンド(apache bench)で実行できます:
ab -c 20 -t 10 https://localhost:4567/
結果はこちらです:

結果からは、Ruby 2.6が2.5よりも速いことがわかります。しかし注目すべきは、JITを有効にするとSinatraが11%も遅くなってしまったことです!
なぜこんなことが起こるのでしょうか?
明確な理由は特定できていませんが、JIT自体のオーバーヘッドや、コードが十分に最適化されていないことが考えられます。
C製プロファイラ(callgrind)を使った調査では、SinatraにおいてJITで最適化されたコード(先ほど確認したコンパイル済みCファイル)の実行割合は非常に低く(2%未満)、一方で8倍の高速化が得られたwhile文では非常に高い(24.22%)という結果が出ました。
whileベンチマークのcallgrind計測結果(JITあり):

Sinatraベンチマークのcallgrind計測結果(JITあり):

この実行割合の差が原因の一部である可能性はありますが、筆者はコンパイラの専門家ではないため、断定的な結論を出すことは避けておきます。
まとめ
MJITは、Ruby 2.6で利用可能になったジャストインタイムコンパイラで、--jitフラグを付けるだけで有効化できます。大きな可能性を秘めた技術であり、単純な小規模プログラムなら大幅な高速化も期待できますが、実アプリケーションで成果を出すにはまだ多くの課題が残っています!
この記事が役に立ったと思ったら、ぜひRuby仲間にもシェアしてください 🙂
最後までお読みいただきありがとうございました。
-
コンピュータ ウイルスとその仕組み
コンピュータウイルスとは? コンピュータ ウイルスは、他のプログラムに付着し、自己複製し、あるコンピュータから別のコンピュータに拡散するマルウェアの一種です。ウイルスがコンピュータに感染すると、自身のコピーを作成し、他のファイルやドキュメントに添付されます。その後、それらのファイルを変更し、拡散を続けます。 ウイルスはコンピュータに密かに感染し、多くの場合、個人のファイルを破壊したり、デバイスを制御したりするように設計されています。コンピュータ ウイルスは自分自身のコピーを作成し、生物学的なウイルスが人から人へと感染するように、デバイスやネットワーク全体に広がります。また、生物学的バージョ
-
Stuxnet(スタックスネット)とは?世界初のサイバー兵器の概要と仕組みを解説
Stuxnetとは? Stuxnet(スタックスネット)は、イランの核関連施設への攻撃を目的として設計・展開されたコンピュータワームです。物理インフラに実際的な影響を与えた、世界初の本格的なサイバー兵器とされており、イランの核濃縮用遠心分離機を標的として、重要な軍事能力に損害を与え、イランの核開発計画に大きな混乱をもたらしました。 Stuxnetの開発者が誰なのか公式には明らかにされていませんが、米国とイスラエル両政府が共同で開発したものだと広く認識されています。イランの核兵器開発計画の進展に対する懸念が高まる中、両政府は原子力研究開発センターへの空爆を含むさまざまな選択肢を検討していました。