くりにっき

フルスタックキュアエンジニアです

Ruby 3.1.2 with YJIT vs Ruby 3.2.0-dev with YJIT

tl;dr;

  • Ruby 3.2.0-devのYJITはRuby 3.1.2のYJITに比べてパフォーマンスが向上して、特にメモリ使用量が大きく削減されている

モチベーション

ISUCONの事前講習*1で講師の人が「RubyはISUCON前になるとパフォーマンス改善系のコミットが入るので、自分たちのチーム(白金動物園)ではいつもruby-headを使っている」と言っていたのですが、Ruby 3.2.0-devだとRust実装のYJITが入っているのでISUCONで使うためにどれくらい性能改善してるか知っておきたかったのが一番大きなモチベーションです。

Ruby 3.2.0-devの何がすごいか

Ruby 3.1系のYJITはC実装でしたが、Ruby 3.2系ではRust実装に変更されています。

techracho.bpsinc.jp

ただしRust実装のYJITは現時点ではpreview版などにも含まれていないため、Rust実装のYJITを利用するためにはRuby 3.2.0-dev( https://github.com/ruby/ruby のmasterブランチ)を利用する必要があります

Ruby 3.2.0-devでRust版のYJITを使う方法

  • Rustの処理系をインストールして --enable-yjit をつけてRubyをビルドする必要がある
    • YJITを使わない場合にはRustのインストールは不要
  • さらに実行時に --yjit を渡す

Ruby 3.2.0-devをYJITを有効化してビルドする方法は id:koic さんの下記エントリが詳しいです

koic.hatenablog.com

計測対象のアプリケーション

達人が教えるWebパフォーマンスチューニング 〜ISUCONから学ぶ高速化の実践:書籍案内|技術評論社(通称ISUCON本) で紹介されてる https://github.com/catatsuy/private-isu を利用しました。

github.com

  • ベンチマーカー
  • 競技用サーバー 2台 *2
    • インスタンスタイプ: c5.large(vCPU 2, メモリ3.75GiB)
    • EBS: gp3 20GB
    • 2台の振り分けは下記
    • pumaのworkerプロセスは3つ
      • vCPU 2なので1.5倍してworkerは3プロセス起動させています *3

どの過去問を使うかにもよると思いますが、他にも何種類かやった感じだと3.2.0-devが3.1.2より悪くなってることはなかったです。(Ruby以外にボトルネックが残ってる状態だとメモリ以外はあまり変化はなかった)

使ったバージョン

Ruby 3.1.2(安定版の最新のRuby)とRuby 3.2.0-dev(7/2時点のmasterブランチでビルドした超最新のRuby)で比較してます。

isucon@isucon-01:~$ ~/local/ruby/versions/3.1.2/bin/ruby --version
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]

isucon@isucon-01:~$ ~/local/ruby/versions/3.2.0-dev/bin/ruby --yjit --version
ruby 3.2.0dev (2022-07-02T10:41:02Z master 7b78aba53a) +YJIT [x86_64-linux]

isucon@isucon-01:~$ ~/.cargo/bin/rustc --version
rustc 1.62.0 (a8314ef7d 2022-06-27)

比較

  • 競技用サーバーをisucon-01, isucon-02のようなホスト名で動かしてDatadogでメトリクスを収集
  • 今回はpumaを動かしてるisucon-01のメトリクスのみ掲載
  • 素の状態だと十分に性能が発揮されないので8時間の素振りでできるだけの改善を行った状態で、最後にRuby 3.2.0-devにしています

CPU

ホスト全体

CPU使用率はあまり変化なし。過去問の種類によっては3.2.0-devでちょっと減ってることがあった(誤差かも?)

Ruby 3.1.2 + YJIT

Ruby 3.2.0-dev + YJIT

pumaのmasterプロセスとworkerプロセスをピックアップ

Datadogのプロセスインテグレーション を使ってpumaのmasterプロセスとworkerプロセスのメトリクスだけ調べたのが下記になります。(具体的にはpsコマンドでとれるCPU使用率をvCPUの数で割った値をグラフに出している)

余談ですがpumaやMySQLやnginxなどISUCONで頻出のミドルウェアはプロセス単位でメトリクス見れるようにしとくとモニタリングが楽ですね。

Ruby 3.1.2 + YJIT

Ruby 3.2.0-dev + YJIT

こっちだとトータルで10%程度CPU使用率が下がってます。(誤差かも?)

Memory

ホスト全体

Ruby 3.2.0-dev + YJITだとホスト全体のメモリ使用量のトップラインがだいたい5%程度削減されています

Ruby 3.1.2 + YJIT

Ruby 3.2.0-dev + YJIT

pumaのmasterプロセスとworkerプロセスをピックアップ

pumaのプロセス単位で見ると劇的に減ってるのが分かると思います。

Ruby 3.1.2 + YJIT

Ruby 3.2.0-dev + YJIT

id:inductor さんが下記ブログで書いてるようにRuby 3.1のYJITだとメモリを喰う問題がありますが、Ruby 3.2.0-devで改善されているのが嬉しいところ。

blog.inductor.me

Ruby 3.2.0-devをデプロイした瞬間のグラフ

Ruby 3.2.0-devをデプロイした瞬間のメモリのグラフを見るとpumaのメモリが一気に減ってるのが分かりやすいです。

1番目のグラフで一瞬サーバ全体のメモリ使用量が増えてるのは多分 bundle install でnative extensionがビルドされていたからだと思います。(Rubyのマイナーバージョンが変わるとgemのインストール先も変わるため)

スコア

Ruby 3.2.0-devにするだけでスコアが1割弱上がりました。

Ruby 3.1.2 + YJIT

{"pass":true,"score":38720,"success":37371,"fail":0,"messages":[]}

Ruby 3.2.0-dev + YJIT

{"pass":true,"score":40574,"success":39244,"fail":0,"messages":[]}

まとめ

  • ビルドにRustの処理系が必要になることを除けばRuby 3.2.0-devのYJITは最高

*1:https://isucon.net/archives/56735884.html

*2:本当はnginxとpumaを分けた上でpumaを2台にして計3台構成にしたかったが、nginxとpumaを別サーバーに分けるとbenchがうまく動かなかったのでこの構成になった

*3:https://zenn.dev/rosylilly/articles/202201-config-turn#workers