RubyとRails向けJITコンパイラ徹底解説:YJIT・MJIT・TenderJITでパフォーマンスを向上させる方法
実行前にコンパイルするのではなく、プログラムの実行時にコンパイルを行う手法があります。これが「ジャストインタイム(JIT)コンパイル」、あるいは「動的翻訳」と呼ばれるものです。
この記事では、まずRuby on RailsアプリケーションにとってJITコンパイルが有効な選択肢となり得る理由を解説し、その後、利用可能な主要なオプション(YJIT、MJIT、TenderJIT)の概要と導入方法をご紹介します。
その前に、まずはJITコンパイルの仕組みから見ていきましょう。
JITコンパイラの仕組み
ジャストインタイムコンパイルとは、プログラムの実行中にコードのコンパイルを行う方式です。
ソースコードを直接翻訳するケースもありますが、一般的にはバイトコードをマシンコードに変換し、それを直接実行する形が取られます。
JITコンパイラを使用するシステムでは、実行中のコードが継続的に分析されます。これにより、コンパイルや再コンパイルによる速度面のメリットが、そのコストを上回るコード部分が自動的に特定されます。
RubyにおけるJITコンパイルのメリット
JITコンパイルは、プログラムをマシンコードに変換する従来の2つの手法――インタプリタ方式と事前コンパイル(AOT)方式――の長所(および短所)を組み合わせたものです。
大まかに言えば、インタプリタの柔軟性と生成コードの高速性を兼ね備えつつ、コンパイルとリンクに伴う追加のオーバーヘッドも発生します。
JITコンパイルは動的コンパイルの一種であり、動的な再コンパイルや特定のマイクロアーキテクチャ向けの最適化といった適応的最適化技術を可能にします。実行時システムが遅延バインディングのデータ型を扱い、セキュリティ保証を課せるため、Rubyのような動的プログラミング言語は、インタプリタ方式やJITコンパイルと特に相性が良いのです。
GCCのような最適化コンパイラは、レジスタ指向アーキテクチャを採用することで命令をより効率的に最適化できます。レジスタベースのアーキテクチャでは、コンパイラは中間表現を対象として動作します。
コンパイル時に命令が中間表現に到達すると、GCCは追加の最適化パスを実行し、CPUが命令をより高速に実行できるようにします。
Ruby向けJITコンパイラ:YJIT、MJIT、TenderJIT
それでは、Rubyで利用できる各種JITコンパイラ――YJIT、MJIT、TenderJIT――について詳しく見ていきましょう。それぞれのセットアップ方法も解説します。
MJIT(Ruby向けメソッドベースJITコンパイラ)
MJITはVladimir Makarovによって実装された、C言語ベースとしては初めてRubyに導入されたコンパイラ方式です。Ruby 2.6で動作し、YARV命令を使用して、頻繁に使われる命令をバイナリコードにコンパイルします。
入出力(I/O)処理がボトルネックになっていないプログラムにおいて、MJITはパフォーマンスを向上させます。
ただし、パフォーマンス面ではYJITの方がこの初期のCベースコンパイラを上回ります。Ruby 3のJITは、MJITの優れた成果の上に築かれた、MRI史上最速のJITなのです。
MJITの使い方
MJITを使用するには、Ruby 2.6で--jitオプションを付けてJITを有効化します。
この指定を省略すると、MJITはエラーを表示します。
Ruby 2.6にはJIT固有の設定一式が含まれており、これを確認することで動作の仕組みを理解できます。ruby --helpを実行して、これらのオプションをチェックしてみてください。
要するに、MJITは別スレッドで非同期に実行され、ある計算が最初の5回実行された後にジャストインタイムコンパイルを開始する仕組みです。
Ruby on Rails向けYJIT
YJITはRuby 3.1とともに登場した比較的新しいJITコンパイラで、多くの改善と高いパフォーマンスが期待できます。ただし、Shopifyが設計したまだ開発途上の実験的プロジェクトであるため、特に大規模なアプリケーションでの使用には注意が必要です。
その点を踏まえた上で言えば、YJITはRuby on Railsアプリケーションのパフォーマンスを確実に向上させます。実際のソフトウェアの多くは、YJITの基本ブロックバージョニングJITコンパイラが提供する高速なウォームアップとパフォーマンス向上の恩恵を受けています。
YJITプロジェクトの一環として、JITコンパイラはCRubyに段階的に組み込まれていく予定であり、将来的にはコード実行の大部分でインタプリタを置き換えることが目指されています。
公式ベンチマーク(「YJIT: Building a New JIT Compiler for CRuby」参照)によると、YJITはデフォルトのCRubyインタプリタと比べて以下の性能向上を達成しています:
- railsbenchで20%向上
- Liquidテンプレートレンダリングで39%向上
- ActiveRecordで37%向上
しかし一方で、留意すべき点もあります:
railsbenchで実行される命令のうち、YJITによって処理されるのは約79%にとどまり、残りはデフォルトのインタプリタで実行されています。
出典:YJIT: Building a New JIT Compiler for CRuby
これは、YJITの現時点での結果をさらに改善すべき余地が多く残っていることを意味します。
それでも、YJITはすべてのベンチマーク(最も難しいものを含む)で少なくともインタプリタと同等以上のパフォーマンスを発揮し、わずか1回の反復だけでほぼピーク性能に到達します。
YJITの使い方
注意:YJITは現在、x86-64プラットフォームのmacOSおよびLinuxに限定されています。また前述の通り、大規模アプリケーションでの使用は(現時点では)推奨されていません。
YJITはデフォルトでは無効になっています。有効化するには、まず--yjitコマンドラインオプションを指定します。
インストール済みかどうかを確認するため、ruby --enable-yjit -vを実行してください。warning: unknown argument for --enable: 'yjit'という警告が表示された場合は、インストールが必要です。
次にirbを開いてRUBY_YJIT_ENABLE=1を設定します。終了すれば、YJITを使う準備は完了です。ruby --enable-yjit -vがruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [arm64-darwin21]のような出力を返せば成功です。
TenderJIT
TenderJITは、設計の大部分をYJITに基づくRuby向けの実験的JITコンパイラです。TenderJITの特異な点は、純粋なRubyで書かれていることです。
これはデモプロジェクトであり、gemとして公開することが目標です。現時点では試してみることはできますが、まだ開発途上である点には留意してください。TenderJITにはRuby 3.0.2以降が必要です。
TenderJITの使い方
TenderJITは現在、メソッドの自動コンパイルには対応していません。メソッドをコンパイルするには、手動でTenderJITを設定する必要があります。
リポジトリをクローンして、以下のコマンドを実行します:
コード内で手動設定を行う必要があります:
TenderJITは対象メソッド内の各YARV命令を読み取り、それをマシンコードへ変換します。
TenderJITのさらなる使用例については、以下の動画をご覧ください:「A JIT compiler for Ruby with Aaron Patterson」、「Hacking on TenderJIT!」。
まとめ
この記事では、Ruby向けの3つのJITコンパイラ――MJIT、YJIT、TenderJIT――とそのセットアップ方法をご紹介しました。どの選択肢も実験的なものであり、それぞれに制限事項があります。
しかしながら、現時点で最も成熟しているのはYJITであり、成長とスケールの可能性も最も大きいと言えます。他のRuby向けJITよりも優れたパフォーマンスを示し、Ruby 3.1.0と共に開発され、CRubyの重要な構成要素になりつつあります。
Ruby向けに独自のコンパイラを作りたい方は、こちらの記事もぜひチェックしてみてください。
それでは、Happy coding!
P.S. Ruby Magicの記事を公開後すぐにお読みになりたい方は、Ruby Magicニュースレターをご購読ください。記事を見逃すことはありません!
Renata Marques
ゲスト執筆者のRenata Marquesは、RubyとJavaScriptを専門とするソフトウェアエンジニア兼コンピュータサイエンティストです。オープンソースプロジェクトへの貢献を通じて開発者コミュニティの支援を楽しんでいます。余暇には映画やテレビ番組の鑑賞・議論、読書、音楽鑑賞、写真撮影、動画制作などを楽しんでいます。
Renata Marquesの記事一覧
-
Rubyのラムダ徹底解説――基本から実践的な応用テクニックまで
Rubyにおいてブロックは非常に重要な存在であり、ブロックなしにこの言語を語ることはできません。では、ラムダはどうでしょうか?ラムダを愛している人はいるのでしょうか?実際のところ、何年もラムダを使わずに過ごすことだって可能かもしれません。まるで昔の時代の遺物のように見えることもあるでしょう。 しかし、それは完全な真実ではありません。少し掘り下げてみると、ラムダには興味深い技が数多く隠されているのです。 この記事では、まずラムダの基本的な使い方から始め、その後、より高度で面白い応用へと進んでいきます。すでに毎日ラムダを使いこなしている方は、ぜひ後半の応用編までスクロールしてください。 ラムダにつ
-
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(メソッドベースのジャストインタイムコンパイラ)」の略称です。 これは具体的にどういう意味でしょうか?