Ruby 3.1でJITを有効化したときにRailsアプリケーションは速くなるのか

## はじめに

[Qiita株式会社 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/qiita)の22日目の担当は、Qiita株式会社CX向上グループの@kyntkです！

https://qiita.com/advent-calendar/2021/qiita

Ruby 3.1で新たなJITであるYJITがマージされました。3.0の時点ではJITを有効化するとRailsのパフォーマンスが低下するということをなんとなく聞いたことがあったのですが、3.1ではどうなるのかが気になったので色々と調べてみました。

## 結論

:::note warn
正直恥ずかしながら、まだ私自身きちんと理解できていないので、必ず添付してある発表内容等を確認してください
また、間違った情報があるかもしれないので、そこはコメントや編集リクエストでください!!!
:::

- Ruby 3.0でMJITを有効にすると、デフォルト設定ではRailsアプリケーションではパフォーマンスの低下が起きうる
  - [Ruby 3.0.0 Released](https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/)
- Ruby 3.1ではパフォーマンスは改善されたが、起動に時間がかかったり、コンパイル中のパフォーマンス低下などの問題も残っているので本番導入する場合は注意が必要
  - [Why Ruby's JIT was slow by Takashi Kokubun - RubyKaigi Takeout 2021](https://rubykaigi.org/2021-takeout/presentations/k0kubun.html)
- Ruby 3.1で新たにYJITがexperimentalで導入されるが、これはRailsであってもパフォーマンスが向上される。ただしまだ対応プラットフォームが限られている
  - [Ruby 3.1.0 Released](https://www.ruby-lang.org/en/news/2021/12/25/ruby-3-1-0-released/)

## 内容について

基本的にRubyKaigi Takeout 2021で話されていた内容を聞いてもらうのがいいと思うのですが、私自身最初に聞いたときに全然理解できなかったので、同じような方がいたら少しでも動画の理解の助けになればと思い書きました。

なのでより正しい情報が気になる方は、以下のセッション動画を見てもらいたいです。

- RubyKaigi Takeout 2021
  - [Why Ruby's JIT was slow by Takashi Kokubun - RubyKaigi Takeout 2021](https://rubykaigi.org/2021-takeout/presentations/k0kubun.html)
  - [YJIT - Building a new JIT Compiler inside CRuby by Maxime Chevalier-Boisvert - RubyKaigi Takeout 2021](https://rubykaigi.org/2021-takeout/presentations/maximecb.html)
- MJITに関する記事
  - [Ruby 3 JIT can make Rails faster. I’ve wondered Why Rails becomes slow… | by k0kubun | Medium](https://k0kubun.medium.com/ruby-3-jit-can-make-rails-faster-756310f235a)

## そもそもRubyにおけるJITコンパイラとは

RubyはCなどのように開発者が実行前にコードをコンパイルすることなく実行できます。しかし、内部的にはプログラム実行時にコンパイルが行われています。ただコンパイルと言っても出力は機械語ではありません。
プログラムは字句解析と構文解析を通して抽象構文木(AST)へ変換されます。そしてこれをコンパイルし、YARV命令列と呼ばれるバイトコード命令列へ変換しています。[^microscope]
![what is JIT compiler](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/4d319b32-dff7-2fb1-e430-ae3021024090.png)
([Rubyのしくみ Ruby Under a Microscope](https://www.amazon.co.jp/dp/B01IGW56CU/ref=dp-kindle-redirect) を参考)

そしてこのYARV命令列はYARV (Yet Another Ruby Virtual Machine) と呼ばれるインタプリタによって実行されます。名前の通り、YARVは仮想マシン (VM) で、セッションなどでもVMという単語が出てきますが、YARVのことを指していることが多いです。

現在のCRuby (MRI) におけるJITコンパイラはこのYARV命令列をコンパイルしています。MJITはこのYARV命令列を、実行中にCにコンパイルをし、さらに生成したCのコードをGCCを用いて機械語に変換してYARVで実行します。これにより処理の高速化を実現しています。また、JITコンパイルすることで実行時の型の情報なども使えるようになるので、コンパイル時の最適化も目指しているようです。

上記で說明したのはMJITの方式で、YJITはコンパイル先がCではなく機械語であったりと、JITコンパイラにもそれぞれ特徴があるようです。

ちなみにMJIT、YJITはデフォルトでは無効化されていて、オプションか環境変数で有効化しないと使えない状態となっています。

[^microscope]: [Rubyのしくみ Ruby Under a Microscope](https://www.amazon.co.jp/dp/B01IGW56CU/ref=dp-kindle-redirect) を参考

## それぞれの特徴をより深堀り

このように、JITコンパイラごとの特徴を知ることで、それぞれが何を得意としているのかの理解が深まります。そのため、もう少しそれぞれの特徴について深ぼってみます。

## MJITについて

MJITのアーキテクチャについては今年のRubyKaigi Takeout 2021のセッションでしっかり説明されていますが、こちらを理解するために過去の経緯をさかのぼって調べました。

https://rubykaigi.org/2021-takeout/presentations/k0kubun.html

まず、初めてJITがRubyにマージされたのはRuby 2.6でした ( [Ruby 2.6.0 Released](https://www.ruby-lang.org/ja/news/2018/12/25/ruby-2-6-0-released/) )。リリースノートには以下のように書かれています。

> JITコンパイラはあらゆるRubyプログラムの実行を高速化することを目的としています。 他言語の一般的なJITコンパイラと異なり、RubyのJITコンパイラはC言語のソースコードをファイルとしてディスクに書き、通常のCコンパイラを用いてネイティブコードに変換することでJITコンパイルを行うという手法を用いています。(参考: MJIT organization by Vladimir Makarov)

またマージされた際に実装者の @k0kubun さんによってこちらに詳しく說明されています。

https://k0kubun.hatenablog.com/entry/ruby26-jit

この記事ではRuby 2.6でマージされるまでの経緯が説明されています。

スタックベースの命令で作られているVM(YARV)をRTL命令化するというプロジェクトと、そのRTL命令上で動作するMJIT、このRTL-MJITをもとにYARV命令をベースとするYARV-MJITへ変わった経緯などが説明されています。

上記の記事を読むとRubyKaigiで出てくる、下記画像に出てくるような単語についての背景知識が分かるので理解がしやすくなると思います。
![Layers of concerns about JIT compiler](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/ce82c3cb-05b9-0f5c-6589-737bebecc867.png)
([Why Ruby's JIT was slow by Takashi Kokubun - RubyKaigi Takeout 2021](https://rubykaigi.org/2021-takeout/presentations/k0kubun.html) の動画11分ごろより引用)

また、以下のスライドでは、(RTL-)MJIT、YARV-MJITの処理の流れについても図解していただいているのでイメージしやすかったです。 (ただ、スタックベースの命令・レジスタベースの命令など、申し訳ないですが、未だにきちんと理解できていない部分もあります。)

<script async class="speakerdeck-embed" data-id="350f765d90614ff39a27086234442c14" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

以下は「Playing with ruby’s new JIT: MJIT」というマージ直後の記事の翻訳ですが、MJITの使い方などもわかって分かりやすかったです。

https://techracho.bpsinc.jp/hachi8833/2018_02_15/52541

よく呼ばれるメソッドを対象にコンパイルをするために、`--jit-min-calls`というオプションで、コンパイルするまでに呼び出された数の制御をしている部分などのイメージがつかめました。

### 高速化に向けて

Rubyはバージョン3に向けてRuby 3x3としてパフォーマンス改善に取り組み、実際Optcarrotなどのベンチマークで高速化を実現できました。ただ、MJITは[Ruby 3.0.0 リリースノート](https://www.ruby-lang.org/ja/news/2020/12/25/ruby-3-0-0-released/)にあるように

> Railsのような、様々なメソッドを満遍なく呼び出すi-cacheへの負荷が大きいワークロードでは、JITがその負荷を大きくしてしまうため性能を改善できる状態にはまだ至っていません

というようにデフォルト設定ではRailsにおいてパフォーマンス改善はできていませんでした。こちらについてなぜ遅かったのかについては以下の記事で說明されています。

https://k0kubun.medium.com/ruby-3-jit-can-make-rails-faster-756310f235a

いくつかの遅くなっている理由に対して複数の仮説が挙げられていますが、最終的にはコンパイルの設定変更によってRailsアプリケーションにおいても高速化できたようです。具体的には、以下の画像のようにVMと比較して103%〜104%の改善が見られたようです。

とはいえ、最終的にはコンパイルの設定変更が要因でしたが、それ以外にもそれまでの3年間の間に様々な改善を積み重ねたことも大きく影響しているようでした。
![Railsbench](https://miro.medium.com/max/595/1*YrtMagJKLJrqa91AdFhOXA.png)
([Why Ruby's JIT was slow by Takashi Kokubun - RubyKaigi Takeout 2021](https://rubykaigi.org/2021-takeout/presentations/k0kubun.html)より引用)

また、現在もいくつかのバグや、Zeitwerk / TracePointとの非互換性など改善ポイントは残しているようで、本番稼働するにはまだハードルも残っていそうです。

### その他の特徴と注意点

改めてRubyKaigi Takeout 2021のセッションに戻ります。こちらのセッションではMJITの特徴について、Rubyの他のJITコンパイラとの比較も踏まえながら說明されています。

https://rubykaigi.org/2021-takeout/presentations/k0kubun.html

MJITの特徴としてまず挙げられているのがMaintainabilityです。
MJITはYARVを使用しているのでVM側での新規実装を自動でサポートできたり、実装の観点でいうと、生成するのはCのコードなのでデバッグがしやすかったりするようです。ただ、最適化についてはGCCがすべてやってくれるわけではないので、やってくれていない部分を実装する必要があるようです。

実行時の最適化に関して、投機的実行やCメソッドのインライン化の說明がありました。投機的実行については、RTLがVMレベルで型情報を使いながら最適化しているのに対してMJITはJITコンパイラレベルで最適化を行っているようです。

またコンパイルの方式は、MJITは並列でコンパイルをしている一方で、YJITはメインスレッドで同期的にコンパイルを実施しているという違いがあります。これによってMITはJITコンパイルにかかる時間が比較的長くなっており、ウォームアップの時間に影響しているそうです。具体的には、1000メソッドのRailsbenchでコンパイル実行のウォームアップに5分かかったというのが説明されています。

またコンパイル時間が長くなることにより、本来はパフォーマンスを向上させたい目的のMJITによりパフォーマンスの低下を引き起こす可能性もあるという問題も指摘されています。

## YJITについて

YJITは3.1.0-preview1の前にマージされたばかりです。こちらも有効化するにはオプションが必要です。また、YJITはまだexperimentalな機能となっており、対応プラットフォームも限られているようです。

https://www.ruby-lang.org/en/news/2021/11/09/ruby-3-1-0-preview1-released/

また、

> YJIT achieves both fast warmup time and performance improvements on most real-world software, up to 22% on railsbench, 39% on liquid-render.

とあり、experimentalでありながらパフォーマンス向上にはとても期待ができます。

YJITについても先日のRubyKaigiのセッションにて詳しい說明がありました。

https://rubykaigi.org/2021-takeout/presentations/maximecb.html

こちらのセッションでは各種ベンチの結果が発表されていますが、リアルワールドに近いベンチだとすべてにおいてインタプリタとMJITよりも高いパフォーマンスを発揮していました。
互換性についてもRubyだけでなくShopifyのCI等も含めてテストが通るレベルになっているようです。
![YJIT bench](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/33e0cbdc-714b-8c93-5412-d038a00e3069.png)
([YJIT - Building a new JIT Compiler inside CRuby by Maxime Chevalier-Boisvert - RubyKaigi Takeout 2021](https://rubykaigi.org/2021-takeout/presentations/maximecb.html)の動画13分ごろより引用)

YJITはRailsアプリケーションでもハイパフォーマンスが出ることを目指して作られました。YJITもMJITと同様にYARVのバイトコードをJITコンパイルしますが、生成するのはCのコードではなく直接機械語に変換するようです。

YJITの特徴として、処理を最適化するためにLazy Basic Block Versioning(LBBV)という手法をとっていることが説明されています。これはMJITのときにも軽く説明されていた、ランタイムの型情報を用いてコンパイル時に型に基づいて最適化されたコードを生成するテクニックの一つです。LBBVについての說明はRubyKaigiのセッションにて図解とともに說明されているのでこちらを見たほうが直感的に理解しやすいです。

### YJITの今後

現時点でもすでにパフォーマンスが改善されていますが、現状は既存のCRubyの実装を大きく変えずに行っており、インタプリタ前提で作られたCRuby上でのパフォーマンスなので、満足のいくものではないようです。パフォーマンスを最大化させるためにはよりパフォーマンスを出しやすい設計に再デザインし、ターゲットを絞って実装しなおす必要があると言っています。

関連して、オブジェクトをよりシンプルに、効果的にするために「オブジェクトシェイプ」についても説明されています。こちらもRubyKaigiのKeynoteとして別のセッションで說明されていましたが、こちらでは說明を省きます。

https://rubykaigi.org/2021-takeout/presentations/chrisgseaton.html

そして最後に、「Ruby Committers vs the World」のセッションで少しだけ言及されていたのですが、もしかしたら今後はMJITを外してYJITだけになる可能性もあるかもしれないです。(以下動画の53:45ごろ)

<iframe width="560" height="315" src="https://www.youtube.com/embed/zQnN1pqK4FQ?start=3225" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

このように、まだexperimentalでありながら今後に期待したいなと思えるものでした。

## まとめ

結論の再掲となりますがまとめます。

:::note warn
正直恥ずかしながら、まだ私自身きちんと理解できていないので、必ず添付してある発表内容等を確認してください
また、間違った情報があるかもしれないので、そこはコメントや編集リクエストでください!!!
:::

- Ruby 3.0でMJITを有効にすると、デフォルト設定ではRailsアプリケーションではパフォーマンスの低下が起きうる
  - [Ruby 3.0.0 Released](https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/)
- Ruby 3.1ではパフォーマンスは改善されたが、起動に時間がかかったり、コンパイル中のパフォーマンス低下などの問題も残っているので本番導入する場合は注意が必要
  - [Why Ruby's JIT was slow by Takashi Kokubun - RubyKaigi Takeout 2021](https://rubykaigi.org/2021-takeout/presentations/k0kubun.html)
- Ruby 3.1で新たにYJITがexperimentalで導入されるが、これはRailsであってもパフォーマンスが向上される。ただし対応プラットフォームが限られている
  - [Ruby 3.1.0 Preview 1 Released](https://www.ruby-lang.org/en/news/2021/11/09/ruby-3-1-0-preview1-released/)

今後もまだまだ改善がされていくと思うので、今後に期待しています。

個人的には引き続きわからない部分を調べて、来年はもう少し理解できているようにしたいと思います。

## 終わりに

明日の[Qiita株式会社 Advent Calendar 2021](https://qiita.com/advent-calendar/2021/qiita)は、 @tanaka_shoya が担当しますのでお楽しみに！

## その他の参考資料

参考資料は文中に記載してありますが、記載していない部分で参考にした資料を追加で記載します。

### YARV

- [Rubyのしくみ　Ruby Under a Microscope](https://www.amazon.co.jp/dp/B01IGW56CU/ref=dp-kindle-redirect)

### MJIT

- Ruby 3におけるMJITのベンチマークや改善ポイントについて說明されている
  - [Ruby 3 JIT's roadmap / RubyConf China 2020 - Speaker Deck](https://speakerdeck.com/k0kubun/rubyconf-china-2020)
- 実装について
  - [かRuby 2.6のJITで実装か検討を行なった最適化集 - Qiita](https://qiita.com/k0kubun/items/e668e272a5817ebe409d)
  - [Ruby 3.0のJIT変更解説 - Qiita](https://qiita.com/k0kubun/items/b2c23aeeac281f486ab6)

### YJIT

- [ruby/yjit.md at master · ruby/ruby](https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md)
- [YJIT: CRuby向けの新しいJITコンパイラを構築する（翻訳）｜TechRacho by BPS株式会社](https://techracho.bpsinc.jp/hachi8833/2021_11_02/112909)
- [MoreVMs’21: “YJIT: Building a New JIT Compiler Inside CRuby” – Maxime Chevalier-Boisvert - YouTube](https://www.youtube.com/watch?v=vucLAqv7qpc&feature=youtu.be)