`ActiveRecord::Relation#extending!` に空呼び出し時の早期リターンを追加
ActiveRecord::Relation#extending! が引数もブロックも受け取らない場合に即座に self を返すよう最適化されました。これにより、merge_multi_values だけでなくすべての呼び出し元が等しくこの最適化の恩恵を受けます。
背景
本PRは #57199(@fatkodima)のフォローアップとして、コードレビューでの提案を実装したものです。#57199 は、テストスイートのプロファイリングにより ActiveRecord::Relation::Merger#merge_multi_values がホットパスで多くの時間を占めていることを発見したことに端を発しています。
プロファイリングの結果、merge_multi_values 内の other.extensions - relation.extensions という集合差演算が大部分を占めていました。リレーションのマージは頻繁に発生する操作ですが、大半のケースでは other.extensions が空であるため、この計算は不要なコストでした。#57199 では merge_multi_values の呼び出し元に unless other.extensions.empty? のガードを追加することで対処しましたが、本PRではそのガードを extending! 本体に移動することでより汎用的な最適化を実現しています。
当初の議論ではメソッド呼び出し自体をスキップする方が extending! 内部の早期リターンよりも高速であるとして、呼び出し元のガードを維持する案もありました。しかし最終的なDiffでは merge_multi_values 側のガードも削除され、extending! 本体の最適化に一本化されています。
技術的な変更
変更は2ファイルにまたがり、最適化ロジックを extending! 本体に集約しつつ、merge_multi_values の記述も簡略化されました。
query_methods.rb: extending! への早期リターンの追加
変更前:
def extending!(*modules, &block) # :nodoc:
modules << Module.new(&block) if block
modules.flatten!
# ...
end
変更後:
def extending!(*modules, &block) # :nodoc:
return self if modules.empty? && !block
modules << Module.new(&block) if block
modules.flatten!
# ...
end
modules.empty? && !block という条件により、引数もブロックも渡されなかった場合は即座に self を返します。これで extending! を呼び出すすべての箇所がこの最適化を自動的に享受できます。
merger.rb: 重複排除ロジックの削除と記述の簡略化
変更前:
unless other.extensions.empty?
extensions = other.extensions - relation.extensions
relation.extending!(*extensions) if extensions.any?
end
変更後:
relation.extending!(*other.extensions)
変更前は other.extensions - relation.extensions で手動の集合差演算を行い、重複を排除していました。extending! 内部では |= 演算子を使ってモジュールのリストを更新しているため、重複排除はすでにそちらで担保されています。手動の集合差演算は不要であり、除去できました。また、空チェックのガードも extending! 本体の早期リターンに委ねられたため、呼び出し元のコードは1行に集約されています。重複排除の正確性はリレーションのテスト(activerecord/test/cases/relation_test.rb の45〜62行目)でカバーされています。
設計判断
最適化ロジックをメソッド本体へ移動する アプローチが採用されました。
「呼び出し元のガードはより速い」という事実がありながら、早期リターンを extending! 本体に移した理由は、汎用性と正確性の保証 にあります。呼び出し元ごとにガードを追加する方式では、将来の呼び出し元が同様の最適化を忘れるリスクが生じます。extending! 本体に早期リターンを持たせることで、すべての呼び出し元が自動的に恩恵を受けます。
PR Descriptionには「The merger retains its own call-site guard」という記述がありますが、最終的なDiffでは unless other.extensions.empty? のガードは削除されています。extending! 本体の早期リターンが同等の役割を担うため、merge_multi_values 側のガードは不要と判断されたものと読み取れます。ホットパスにおけるメソッド呼び出しのオーバーヘッドは extending! 本体内の早期リターンで十分に吸収できるという判断が、最終的なコードに反映されています。
まとめ
本PRは、最適化ロジックを適切な抽象レイヤーに配置するというリファクタリングの原則を体現しています。extending! 本体に早期リターンを持たせることで、既存コードの簡略化と将来の呼び出し元への自動的な恩恵を同時に実現しており、コードの保守性と実行効率を両立させた判断といえます。