は、RubyのYJITでどんな挑戦をしてきたかの話でした。
最初はRubyのinstructionを1:1で機械語に置き換えるところか始めました。 Railsでは遅くなりました。 つぎにJIT部分をフロントエンドとバックエンドに分けました。
もう、この辺でわかりません。 フロントエンドとバックエンドにわけるとなにがうれしいのか、そもそもなんで1:1の変換で遅くなるのか? わかってないなりに続きを聞きました。
生成した機械語のコードが条件分岐を増やすので、CPUの予測がはずれやすくなりました。 その対策としてLazy Basic Block Versioningを導入しました。
「Lazy Basic Block Versioning」はJITに関連した発表で、ちょいちょい聞く単語です。 説明してくれてうれしいです。 わかりません。 なんかstubを使うらしいです。 ある変数の型が実行中に変わることが少ないので、変わらないと仮定して、stubでキャッシュして使い回す?
YJITはRubyよりinstructionsを減らしました。
instructionsもよくわからないんです。 Rubyがinstructions持っているのはわかるんですが、YJITも持っているのがわかりません。 YJITがinstructionsと機械語と持つようになったのが、フロントエンドとバックエンドにわけたという意味なのでしょうか?
- cfp-sp (stack pointer)
- cfp-pc (program counter)
サンプル見せて説明してくれたのですが、よくわかりません。 stackのなかみのメモリに書き出すみたいです。 でも、どんなときに書き出すのかとか、書き出すとなにがうれしいのかなどわかりませんでした。
追記
理解の参考になりそうなツイートを集めました。
「YJITの歴史。First Designはインタープリタに非常に近い動作原理だった。Ruby ByteCodeを1行ずつ実行してexecution logに書き出す。RubyCodeは上から下に流れるが、機械語はジャンプバックがある」 #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
最初期の実装の話。Ruby の Instruction Seq をシミュレートしてマシンコード生成してたそう #rubykaigi
— Watson (@watson1978) September 10, 2022
「最初のデザインでは実行ログを上から下に書き出すので説明しやすく速い。良さそうに見えたが、optcarrotベンチは7%向上したもののrailsbenchは7%遅くなった。もちろん、NESエミュレータとwebアプリケーションは異なる」 #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
Rubyのinsnsの動きと一致する様にアセンブラの上でのプログラムカウンターの並びを調整できれば、プログラムカウンターのインクリメントだけで処理できて早くなるかもっていう理屈か。 #rubykaigi
— joker1007 (アルフォートおじさん) (@joker1007) September 10, 2022
この図のFrontendとBackendは、Webのフロント/バックじゃなくて命令セット実行するアキュムレータがフロント、メモリ・演算器がバックエンドって分類っぽいな #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
frontend と backend の意味が多分、よく知られているものと違うという(CPU architecture) #rubykaigi
— _ko1 (@_ko1) September 10, 2022
で、backendの演算器での演算を待ってる間にフロントエンドが待ち状態になるから遅いって話か。直列化しちゃったことでパイプライン化が効かなくなったって話かな #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
「YJITでoptcarrotはbackend boundが悪化してそれ以外は向上したが、railsbenchはfrontend boundが悪化してそれ以外は向上した」 #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
エントリポイントが増えたことで投機実行が効きづらくなった(エントリポイントの意味はわかってない、メモリアクセスの実行? #rubykaigi
— expa / Shu Oogawara (@expajp) September 10, 2022
LBBVは、ちょっとずつコンパイルするテクニックなのよ #rubykaigi
— わいたなか (@ytnk531) September 10, 2022
今さらだけど、CPUアーキテクチャでは
— expa / Shu Oogawara (@expajp) September 10, 2022
* 命令フェッチとデコードがフロントエンド
* 命令実行がバックエンド
と言うみたい #rubykaigi
「Lazy Basic Block Versioning。stubを使ってlazyにする。定義時点ではメソッドの中身をstubに飛ばすようにしておき、bodyの評価タイミングで書き換える」 jumpが複数あってunlessかけてるところはcustom=[]に当たるところかな? Rubyコードレベルと命令列レベルの頭を切り替えるのむずい #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
つまりLBBVで生成される並びにするだけでrailsbenchに効くという話になるのかな #rubykaigi
— k0kubun (@k0kubun) September 10, 2022
なんでitselfメソッドなのか、と思ったけど、もともと Object#itself があり、上書きされた場合の挙動の説明として使ったのかな https://t.co/oYhEH4gGpN #rubykaigi
— うたがわきき (@utgwkk) September 10, 2022
事前に分岐箇所含めて機械語に変換してたらオーバーヘッドが多すぎるし、機械語をメモリに展開しておく量も多くなるから、実行時に評価しながら機械語に変換するよ。分岐を予測して攻撃するテクニックもあったし、分岐は 90% くらい偏っているよって話 #rubykaigi
— Watson (@watson1978) September 10, 2022
最初のcallタイミングでインラインキャッシュのためにデータ型を推測しちゃうよ(なので違うデータが来るとregenerateが必要になることがある)って話をしている? ヒット率が92%以上あるから、たまに外してもトータルでは得って話かな #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
「General Strategy。可能な限り生成する方針は継続。Dynamic Operationではスタブを使う(なんかもう一個項目あったけど追えなかった)」 #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
#rubykaigi たいていのコード(中の変数)は実行時に型が変化しないので、呼び出しのたびに型チェックとかが起こるのを、うまく覚えておいて最適化できる、という話っぽい
— Windymelt (@windymelt) September 10, 2022
ほとんどの処理はmonomorphicだから、出来るだけ生成したコードの中で処理が出来る様にして、どうにもならん時にinterpreter命令に戻るって感じにしたのかな。これで実行命令数自体が減ったという感じか。 #rubykaigi
— joker1007 (アルフォートおじさん) (@joker1007) September 10, 2022
perfでrailsbenchのstat取るとIntepreterよりYJITはInstructionsが1割強減って、Instruction per cycleも向上したらしい。 #rubykaigi
— 黒曜@Leaner Technologies (@kokuyouwind) September 10, 2022
20220918 追記
Ruby が YJIT でなんで速くなるのか? Lazy Basic Block Versioning をサクッと理解してみた - estie inside blog