SmartHR Tech Blog

SmartHR 開発者ブログ

SmartHR最大のRailsアプリケーションでYJITを有効化しました

こんにちは、SmartHR プロダクトエンジニアのB6です。
YJITが本番環境で安定して使える状態になってから、様々な場所でYJITの効果を耳にしてきました。
このたび、「基本機能」と呼ばれるSmartHR最大のRailsアプリケーションにもYJITの導入を完了しました。1 本記事では、その導入結果と実施した対応について共有いたします。

導入環境

YJIT導入時のRuby、Railsのバージョンは以下の通りです。

  • Ruby 3.3
  • Rails 7.1

YJIT有効化の設定

ここでは、YJIT有効化に向けて行った設定を紹介します。 Rails 7.2からはYJITがデフォルトで有効化されます。

Rails.application.config.yjit
#=> true

ただ、我々はRails 7.1なので、まだこの方法を利用できません。そこで下記のようなイニシャライザを用意し、その中でRubyVM::YJIT.enableを呼ぶことで有効化するようにしました。 これは、Railsにかつて用意されていたenable_yjit.rbを参考にしたものです。

Rails.application.config.after_initialize do
  RubyVM::YJIT.enable(stats: true)
end

また、YJITの有効性を確認するために、統計情報を出力するようにしました。app/controllers/application_controller.rbに下記のようなメソッドを用意し、ログ出力しています。2

before_action :logging_yjit_stats

def logging_yjit_stats
  return if defined?(RubyVM::YJIT).nil? || !RubyVM::YJIT.enabled?
  return unless Random.rand(10000) < 1

  stats = RubyVM::YJIT.runtime_stats.slice(:code_region_size, :yjit_alloc_size, :ratio_in_yjit).map { |k, v| "#{k}: #{v}" }.join(", ")
  Rails.logger.info(" [YJIT]: #{stats}")
end

YJIT導入結果 ── 最大18%程度減少

YJIT導入後、APMやGoogle Cloudでメトリクスおよびログを監視しました。以下がその詳細です。

レスポンスタイムの減少

以下は、YJIT有効化後、24時間のレスポンスタイムのp90(90-percentile)・p50(50-percentile)を1週間前のデータと比較した結果です。 それぞれ実線がYJIT有効化後のデータ、点線がその1週間前のデータを表しています。

p90、p50、平均のレスポンスタイムの時系列グラフ
p90、p50、平均のレスポンスタイム

表にしたものがこちらです。

メトリクス YJIT有効化前 YJIT有効化後 変化率
p90 656 ms 594 ms -9.47%
p50 164 ms 133 ms -18.90%
平均 305 ms 288 ms -5.57%

全体的にレスポンスタイムが改善されたことがわかります。特にp50では18.9%の改善が見られました。

メモリ使用率 ── 5%程度増加

以下は、Google Cloudでのメモリ使用率を比較したものです。濃い青の線(40%〜43%)はYJIT有効化後のデータ、薄い青の線(34%〜38%)は1週間前のデータを示しています。

p50のメモリ使用率の時系列グラフ
p50のメモリ使用率

メモリ使用率は4〜5%程度増加しました。事前にYJITが作るコードのサイズ(yjit-exec-mem-size)は48MiBであり、メタデータやワーカー数も含めるとその5〜6倍ほどの消費を見込んでいましたが、メモリ使用量に余裕があったため問題ありませんでした。 Ruby 3.3ではRuby 3.2と比べてYJITのメモリ使用量が削減されたこともあり、その恩恵を受けることができています。

Ruby 3.4からは、YJIT メモリの総使用量を制限するための--yjit-mem-sizeオプションが追加されています(デフォルト128MiB)。 yjit-mem-sizeはYJITが作るコードのみを対象としたyjit-exec-mem-sizeと異なり、メタデータを含むすべてのメモリ使用量に関する設定になります。 現在のログデータ(「ログ ── YJIT統計の結果」参照)を見ると、メタデータを含むYJITのメモリ使用量は128MiB前後で収まっています。 そのため、Rubyのバージョンを3.4に上げる際にも--yjit-mem-size=128のデフォルト値をそのまま適用できそうと考えています。

ログ ── 約98%のコードがYJITにより実行

ログは下記のような出力が見られました。傾向として、インスタンスが立ち上がってから10〜20分ほどでcode_region_sizeが上限である48MiBになり、ratio_in_yjitも97〜98%前後で安定するようになりました。

INFO 2025-03-21T11:06:31.022339665Z [YJIT]: code_region_size: 50331648, yjit_alloc_size: 82905988, ratio_in_yjit: 98.16429873461271
INFO 2025-03-21T11:06:45.270518822Z [YJIT]: code_region_size: 50331648, yjit_alloc_size: 83246215, ratio_in_yjit: 98.38674989790121
INFO 2025-03-21T11:06:46.803211522Z [YJIT]: code_region_size: 50331648, yjit_alloc_size: 80377553, ratio_in_yjit: 98.12828293174877

YJIT導入時に行ったその他の取り組み

YJITの導入にあたり、合わせて行った取り組みを紹介いたします。

Rails、Rubyへのコントリビュート

前述の通りRails 7.2以降では、YJITの有効・無効をRailsの設定でconfig.yjit = false/trueのように管理することになります。 ただし、bool値のみの設定であり、config.yjit経由で統計やログなどのオプションを直接設定する方法が提供されていませんでした。 そこで、{ stats: true }のようにハッシュで渡せるようにすることで、オプションとともにYJITを有効化できるようにする、という内容のPRを作成しました。 今後は統計情報の有効/無効だけでなく、mem_sizeなどのオプションも設定することができるようになりそうです。 github.com

下記のようにconfig.yjitを指定できます。

config.yjit = true 
config.yjit = { stats: true }  # YJITを有効化し、かつオプションを渡す
config.yjit = false  

また、RubyVM::YJIT.enableのドキュメントでパラメータ名が誤っていたため、こちらにもささやかなコントリビュートをしています。 github.com

db:migrateなど特定のタスク・特定の環境でのYJIT無効化

先述の通り、今回イニシャライザ内のRubyVM::YJIT.enableを用いてYJITを有効にしましたが、実際には一部の条件では無効になるように条件を加えています。 具体的にはdb:migrateや、使い捨てのrakeタスクなど短時間で終わるものはあまり恩恵を受けられないのでYJITを無効化したほうがよいのではないか、という議論を経て、下記のようなイニシャライザになりました。

script_dir = "script/"
inside_yjit_disable_task = ARGV.any? { |arg| arg.include?(script_dir) } || Rake.application.top_level_tasks.include?("db:migrate")

if !inside_yjit_disable_task
  Rails.application.config.after_initialize do
    RubyVM::YJIT.enable(stats: true)
  end
end

ただし、Rails 7.2にアップデートした際にはこのようなカスタマイズは難しくなるかもしれません。アップデートする際にあらためてこの制限の必要性を検討する予定です。

また、Rails 8.1からはdevelopment環境やtest環境ではYJITはデフォルトで無効になるため、今回の対応でも同環境では無効になるようにしています。

まとめ

SmartHR最大のRailsアプリケーションにおいてYJITを有効にしたことで、パフォーマンスの向上が見られました。 なお、YJITを有効化するにあたっては、すでにインターネット上に多くの知見があり、さまざまな企業の事例を参考にさせていただきました。

今年のRubyKaigi 2025では次世代 Ruby JITとしてZJITが登場するようです。詳細はまだわからないですが、どのようなものになるのかとても興味深いです。 これからのRubyの進化への期待と、Rubyコミッターの方々への感謝とともに記事を締めくくりたいと思います。

We Are Hiring!

SmartHR では一緒に SmartHR を作りあげていく仲間を募集中です! 少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!

hello-world.smarthr.co.jp


  1. SmartHRは、従業員情報を管理する「基本機能」アプリケーションを中心に、その周辺機能ごとのアプリケーションを連携させたマイクロサービスアーキテクチャを採用しています。一部のアプリケーションではすでにYJITを導入しています。
  2. SmartHRでAPMとして使用しているNewRelicは、YJITの統計情報はまだサポートしていません。本当はミドルウェアを用意してメトリクスとして送信するのが良さそうですが、今回はそこまではしませんでした。