YJITについて説明するときに使えそうな資料の引用まとめ

RubyKaigi 2023のKeynoteでYJITの過去について振り返られていました。

https://rubykaigi.org/2023/presentations/maximecb.html#day2

<iframe src="https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/embed?start=false&loop=false&delayms=3000" frameborder="0" width="960" height="569" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" lazy="true"></iframe>

このセッションでは、MaximeさんがどのようにYJITを開発して、パフォーマンスを計測し、実際にShopifyで本番稼働させて10%スピードアップするまでに至ったかの話がされました。

https://shopify.engineering/ruby-yjit-is-production-ready

これまで、RubyKaigi Takeout 2021以降、毎年RubyKaigiにてYJIT関連のセッションがあり、たくさんの説明がされています。
YJITについて話すときに簡単に資料を引用できるようにまとめてみました。

## YJITが何を実現するものなのか

https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/present?slide=id.g2a799bba62adb1c5_5
P.20
> - Double-digit speedups on real-world Ruby software
>     - Focus on web workloads, Ruby on Rails

すでにMRIに入っていたMJITの手法では、大規模のアプリケーションになると、スピードや起動の時間などがボトルネックになることがわかった。YJITは特にRuby on Railsで作られたアプリケーションのパフォーマンス向上を目指して作られた。

https://qiita.com/kyntk/items/beeb8f5376dd236e54b6

## YJITの特徴

https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/present?slide=id.g22623d86a96_0_481
P.21
> - Lazy code generation
> - Linearization of machine code
> - Run-time value promotion
> - Type-based specialization of generated code
> - Speculative optimization and deopimization
> - Polymorphic inline caches

https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/present?slide=id.g22623d86a96_0_7
P.23

> - Port YJIT to Rust
> - New backend, ARM64, native Apple/RPi support
> - Finer-grained constant cache invalidation
> - Many performance optimizations
> - Memory usage optimization & code GC
> - Object shapes & YJIT support
> - VWA & YJIT support
> - Worldwide SFR deployment
> - YJIT is no longer marked as experimental

それぞれの内容は以下に記載。

## MicroJIT Prototype

YJITを作る前のPrototypeとして作られたJIT。

https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/present?slide=id.g22623d86a96_0_590
P.50

> - Concept:
>     - Generate code for chains of multiple YARV instructions (no branches)

ISeqは直列に命令が並んでいるが、Machine Codeをメモリに展開する時には順番に並んでおらず、実行時にjumpが発生している。これを解消するデザインとした。

https://rubykaigi.org/2022/presentations/alanwusx.html#day3
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/734dc8d9-48f5-9b7f-2ad5-81ebe1c1d67e.png)

シンプルになりMicroJITはoptcarrotでは約10%速くなったが、railsbenchでは遅くなってしまった。
railsbenchだと様々なRubyコードを実行するので、たくさんのMachine Codeを読み込む必要がある。

> Why was it slower?
> - Lots of frontend pressure already
> - Jumping to generated code taxes the frontend even more

プロトタイプの実装では、インタプリタのコードと、生成されたコードのjumpが多く発生してしまった。

## Lazy Basic Block Versioning

https://rubykaigi.org/2021-takeout/data/slides/Maxime-YJIT-RubyKaigi-2021-slides.pdf
P.13
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/ad0d8683-8e46-d52b-220c-a1ae4232e0a2.png)

メソッド単位ではなく、Basic Block単位でのコンパイルを行う。
実行時の型の情報を用いて型の特定をすることで、コストのかかるタイプアナリシスをしない。(AOTコンパイラと比較したJITコンパイラの強み)

### Linearization of machine code

https://rubykaigi.org/2021-takeout/data/slides/Maxime-YJIT-RubyKaigi-2021-slides.pdf
P.24
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/b816852b-5f22-66ba-4069-9a05045d72b8.png)

分岐を減らしてストレートにしている。
その上、各ブランチ(Basic Block)のコード生成を遅延させ、必要になってからコードを生成している。

### Lazy code generation

<iframe width="560" height="315" src="https://www.youtube.com/embed/EMchdR9C8XM?start=846" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen lazy="true"></iframe>
14:06秒〜

stubsを用意しておき、実行するときに実際のコードを生成する

https://rubykaigi.org/2021-takeout/data/slides/Maxime-YJIT-RubyKaigi-2021-slides.pdf
P.39 ~ 41
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/2716e0d2-0ba0-856d-0430-448e14741ff9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/b77a9b79-3d70-bc4d-154d-a0a5f518fa1d.png)

異なるクラスが渡ってきた時に、改めてコードが生成される。最初に生成されるコードが最も速い。一般的に90%近くの割合で同じクラスで呼び出されるので、再利用できる確率が高いというデータがあるらしい。[^1]

[^1]: https://en.wikipedia.org/wiki/Inline_caching

## New backend, ARM64, native Apple/RPi support

https://docs.google.com/presentation/d/1xJ4CwS13Tu2inO2-YxtSNhYmi2y8VDyW4QRpan0fKV0/edit#slide=id.g14a05e20d54_0_33
P.27
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/497ccc7f-4704-bbfb-7453-4a08fba72f04.png)

各プラットフォームに対応するように、IRという中間表現を挟むようにした。
ISeq → IRでLBBVが実装されており、IR → 各Machine Codeに変換するときにプラットフォームごとに最適化をしている。

### Speculative optimization and deopimization

https://youtu.be/EMchdR9C8XM?t=1457
24:17

YJITからインタプリタにフォールバックをするときに、各stateをreconstructすること。

## Object shapes

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

https://rubykaigi.org/2022/presentations/jemmaissroff.html#day2

https://docs.google.com/presentation/d/1tVWltL9-jcrPuTrxjGz1YAxqnYnZ8z1_ipYms6HWSfw/edit#slide=id.g1516577c4f2_0_1438
P. 166
> - Increased cache hits
> - Decreased code complexity
> - Beneficial for JITs
> - Performance

異なるクラスでも、同じインスタンス変数を持つオブジェクトは同じShapeになることがある。
Shapeをキャッシュキーに使うとキャッシュヒットしやすくなる。
インスタンス変数をセットするときに確認することが減る。
JIT時にも命令数、チェックが減り、メモリ参照する回数が減る。

Object Shapesの本来のモチベーションはスピードではなくてspaceだが、上記の理由でスピードアップにも繋がっている。

<!--

## VWA (Variable Width Allocation)

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

https://shopify.engineering/ruby-variable-width-allocation

-->

## Performance

https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/present?slide=id.g2399b6ad7f1_0_144
P.63
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/7277a932-be53-c6bf-e38c-20536f9127c9.png) ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/12dd06eb-4523-57a6-1692-d8a51de1d2b1.png)

## 今後

https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/present?slide=id.g22623d86a96_0_532
P.106
> - Warm-up time
> - Memory usage