RubyKaigi 2023のKeynoteでYJITの過去について振り返られていました。
このセッションでは、MaximeさんがどのようにYJITを開発して、パフォーマンスを計測し、実際にShopifyで本番稼働させて10%スピードアップするまでに至ったかの話がされました。
これまで、RubyKaigi Takeout 2021以降、毎年RubyKaigiにてYJIT関連のセッションがあり、たくさんの説明がされています。
YJITについて話すときに簡単に資料を引用できるようにまとめてみました。
YJITが何を実現するものなのか
- Double-digit speedups on real-world Ruby software
- Focus on web workloads, Ruby on Rails
すでにMRIに入っていたMJITの手法では、大規模のアプリケーションになると、スピードや起動の時間などがボトルネックになることがわかった。YJITは特にRuby on Railsで作られたアプリケーションのパフォーマンス向上を目指して作られた。
YJITの特徴
- Lazy code generation
- Linearization of machine code
- Run-time value promotion
- Type-based specialization of generated code
- Speculative optimization and deopimization
- Polymorphic inline caches
- 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。
- Concept:
- Generate code for chains of multiple YARV instructions (no branches)
ISeqは直列に命令が並んでいるが、Machine Codeをメモリに展開する時には順番に並んでおらず、実行時にjumpが発生している。これを解消するデザインとした。
https://rubykaigi.org/2022/presentations/alanwusx.html#day3
シンプルになり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
メソッド単位ではなく、Basic Block単位でのコンパイルを行う。
実行時の型の情報を用いて型の特定をすることで、コストのかかるタイプアナリシスをしない。(AOTコンパイラと比較したJITコンパイラの強み)
Linearization of machine code
https://rubykaigi.org/2021-takeout/data/slides/Maxime-YJIT-RubyKaigi-2021-slides.pdf
P.24
分岐を減らしてストレートにしている。
その上、各ブランチ(Basic Block)のコード生成を遅延させ、必要になってからコードを生成している。
Lazy code generation
14:06秒〜stubsを用意しておき、実行するときに実際のコードを生成する
https://rubykaigi.org/2021-takeout/data/slides/Maxime-YJIT-RubyKaigi-2021-slides.pdf
P.39 ~ 41
異なるクラスが渡ってきた時に、改めてコードが生成される。最初に生成されるコードが最も速い。一般的に90%近くの割合で同じクラスで呼び出されるので、再利用できる確率が高いというデータがあるらしい。1
New backend, ARM64, native Apple/RPi support
https://docs.google.com/presentation/d/1xJ4CwS13Tu2inO2-YxtSNhYmi2y8VDyW4QRpan0fKV0/edit#slide=id.g14a05e20d54_0_33
P.27
各プラットフォームに対応するように、IRという中間表現を挟むようにした。
ISeq → IRでLBBVが実装されており、IR → 各Machine Codeに変換するときにプラットフォームごとに最適化をしている。
Speculative optimization and deopimization
https://youtu.be/EMchdR9C8XM?t=1457
24:17
YJITからインタプリタにフォールバックをするときに、各stateをreconstructすること。
Object shapes
- Increased cache hits
- Decreased code complexity
- Beneficial for JITs
- Performance
異なるクラスでも、同じインスタンス変数を持つオブジェクトは同じShapeになることがある。
Shapeをキャッシュキーに使うとキャッシュヒットしやすくなる。
インスタンス変数をセットするときに確認することが減る。
JIT時にも命令数、チェックが減り、メモリ参照する回数が減る。
Object Shapesの本来のモチベーションはスピードではなくてspaceだが、上記の理由でスピードアップにも繋がっている。
Performance
https://docs.google.com/presentation/d/1d6o1ChSKLDwcu_ghVKlnjtwi6rb3cBEwk-QOEEoAT4s/present?slide=id.g2399b6ad7f1_0_144
P.63
今後
- Warm-up time
- Memory usage