非STIモデルの`new`メソッドを最適化してRuby 4.0の高速化パスを有効化

rails/rails

ActiveRecord::Base.newが最適化され、STI(単一テーブル継承)を使用していないモデルで不要なSTIチェックが条件付きで除去されるようになりました。この変更により、Ruby 4.0の「fast path allocation」が有効化され、非STIモデルのオブジェクト生成が15~17%高速化されます。

背景

ActiveRecord::Base.newメソッドをオーバーライドして、STIモデル用のランタイムチェックを実装していました。このメソッドは「type」カラムを検査し、適切なサブクラスのコンストラクタを呼び出します。しかし、STIを使用していないモデルにとって、このチェックは不要なオーバーヘッドでした。さらに、Ruby 4.0では、オーバーライドされたメソッドが「fast path allocation」最適化の恩恵を受けられないという問題がありました。

ruby/ruby#13080で導入されたRuby 4.0の最適化は、Class#newがデフォルトのアロケータメソッドであることを検出すると、opt_new命令を使用してオブジェクトを直接スタックにプッシュします。しかし、Active Recordが.newをオーバーライドしているため、この最適化が適用されませんでした。

技術的な変更

activerecord/lib/active_record/inheritance.rbnewメソッドが再構成され、STIを使用しないモデルでは不要なチェックが回避されるようになりました。

変更前:

def new(attributes = nil, &block)
  if abstract_class? || self == Base
    raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
  end

  if has_attribute?(inheritance_column)
    subclass = subclass_from_attributes(attributes)

    if subclass.nil? && base_class?
      subclass = subclass_from_attributes(column_defaults)
    end
  end

  if subclass && subclass != self
    subclass.new(attributes, &block)
  else
    super
  end
end

変更後:

def new(attributes = nil, &block)
  if abstract_class? || self == Base
    raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
  end

  if has_attribute?(inheritance_column)
    subclass = subclass_from_attributes(attributes)

    if subclass.nil? && base_class?
      subclass = subclass_from_attributes(column_defaults)
    end

    if subclass && subclass != self
      subclass.new(attributes, &block)
    else
      super
    end
  else
    super
  end
end

変更前のコードでは、if has_attribute?(inheritance_column)ブロックでサブクラスを決定した後、そのブロックの外側でif subclass && subclass != selfによる分岐が行われていました。変更後は、この分岐をif has_attribute?(inheritance_column)ブロック内に移動することで、STIを使用しないモデルではsubclass変数の判定すら行われなくなりました。これにより、STIを使用しないモデルでは、不要なsubclass_from_attributesの呼び出しだけでなく、その後の条件判定も完全に回避されます。

スキーマロード時の最適化

load_schema!reload_schema_from_cache メソッドが追加され、スキーマロード時に.newメソッドの最適化が行われるようになりました。

def load_schema!
  super
  optimize_new_allocation
end

def reload_schema_from_cache(*)
  if @_new_optimized
    singleton_class.remove_method(:new)
    @_new_optimized = false
  end
  super
end

optimize_new_allocation メソッドは、モデルがSTIを使用せず、.newが再定義されていない場合に以下の処理を実行します:

def optimize_new_allocation
  return if _has_attribute?(inheritance_column)
  return if singleton_class.method_defined?(:new)
  return if @_new_optimized

  define_singleton_method :new, Class.instance_method(:new)
  @_new_optimized = true
end

この処理により、Class.newが直接呼び出されるようになり、Ruby 4.0の「fast path allocation」が有効化されます。スキーマのリロード時には、最適化が一旦解除され、再度optimize_new_allocationが実行される設計です。

テストカバレッジの拡充

activerecord/test/cases/fast_path_allocation_test.rb が新規追加され、以下のシナリオがテストされています:

  • 非STIクラスが最適化されること
  • 抽象スーパークラスを持つ非STIクラスが最適化されること
  • STIクラスとそのサブクラスが最適化されないこと
  • .newを再定義したクラスが最適化されないこと
  • 動的に.newを再定義した場合に最適化が解除されること

テストでは、Rubyがデバッグビルドの場合にopt_new_hitopt_new_missのデバッグカウンタをチェックし、fast path allocationが正しく動作していることを検証します。

パフォーマンスへの影響

ベンチマーク結果によると、STIを使用せず.newを再定義していないモデルでは、以下の性能向上が確認されました:

Ruby 4.0.1:

  • YJITなし: 1.15倍高速(68.7k vs 59.6k i/s)
  • YJITあり: 1.17倍高速(197.2k vs 168.4k i/s)

Ruby 3.4.8:

  • YJITなし: 1.13倍高速(77.5k vs 68.8k i/s)
  • YJITあり: 1.06倍高速(218.3k vs 206.7k i/s)

Ruby 3.3.10:

  • YJITなし: 1.10倍高速(68.8k vs 62.4k i/s)
  • YJITあり: 1.09倍高速(173.6k vs 159.1k i/s)

STIモデルや.newを再定義したモデルでは、パフォーマンスの変化はほぼ誤差範囲内であり、後方互換性が維持されています。

設計判断

スキーマロード時の最適化 という設計が採用されました。load_schema!reload_schema_from_cacheのフックポイントで最適化の適用と解除を制御することで、モデルの状態変化に応じた動的な最適化を実現しています。

@_new_optimizedフラグ により、最適化が既に適用されているかを追跡します。スキーマリロード時にはこのフラグをチェックし、最適化が適用されていた場合はsingleton_class.remove_method(:new)で一旦解除してから、再度最適化の適用可否を判断します。

条件分岐の最小化 も重要な判断です。optimize_new_allocationメソッドは3つの条件(STIの使用、.newの再定義、既に最適化済み)をチェックするだけで、最適化の適用可否を判断します。これにより、ランタイムでの不要な処理を排除しています。

まとめ

本PRは、STIを使用しないActive Recordモデルのオブジェクト生成を最適化し、Ruby 4.0の「fast path allocation」を活用可能にした変更です。スキーマロード時に.newメソッドを条件付きでClass.newに置き換えることで、不要なSTIチェックを除去し、15~17%の性能向上を実現しています。STIモデルや.newを再定義したモデルへの影響はなく、後方互換性を維持しながら、大多数の非STIモデルに最適化の恩恵をもたらす実装といえます。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
4回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術詳細・設計判断(各論)、まとめ(結論)という「総論→各論→結論」の構成が明確に適用されており、非常に分かりやすい記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトやGitHubのPR/Issueへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Active Recordの内部実装やRubyの最適化に関する専門的な内容を、前提知識を持つエンジニア向けに適切な粒度で解説できています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論・各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。これにより、記事の論理構造が明快になっています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事で引用されているコードは、PRのDiff内容を正確に反映しています。変更前後のコード比較も、技術的な変更点を理解する上で効果的です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「fast path allocation」や「STI」、「singleton_class」などの技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

最適化のメカニズムやその効果に関する説明は、PR情報とDiffの内容に裏付けられており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPRのDescriptionやベンチマーク結果に基づいており、ハルシネーション(捏造)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号、パフォーマンス向上率(15-17%)、ベンチマークの具体的な数値(i/s)、Rubyのバージョン番号などが、PR情報と完全に一致しています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRの目的である「STIチェックの条件付き除去」とその結果である「高速化パスの有効化」を的確に要約しており、内容と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事で言及されているRubyのバージョンや最適化に関する情報は、すべてPR内で提供されている情報源に基づいており、PRにない外部知識の捏造はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内には曖昧な時間表現はなく、事実関係が正確に記述されています。