Ruby 3.2の最適化を活用したハッシュ値計算の改善
Railsでは、Ruby 3.2で導入された配列リテラルの最適化を活用し、各種データ構造の#hashメソッド実装が改善されました。XOR演算による手動のハッシュ結合から配列リテラルを使った実装に切り替えることで、配列の一時的なメモリ割り当てを回避し、パフォーマンスが向上します。
背景
Ruby 3.2より前のバージョンでは、[a, b, c].hashのような配列リテラルに対する#hash呼び出しは、配列オブジェクトを実際にメモリ上に構築していました。そのため、複数の値からハッシュ値を計算する際は、XOR演算(^)を使って手動で結合する実装が効率的でした。ruby/ruby#6090により、Ruby 3.2ではopt_newarray_sendという特殊命令が導入され、配列リテラルに続くhash、min、maxメソッドの呼び出しが最適化されました。この命令は配列オブジェクトの割り当てを回避し、要素を直接スタック上で処理します。
Railsの多くのクラスでは、複数のインスタンス変数を組み合わせたハッシュ値計算に手動のXOR演算を使用していましたが、Ruby 3.2以降では配列リテラルを使う方が効率的になりました。
技術的な変更
ActiveRecord::ConnectionAdapters::Columnの#hashメソッドが、XOR演算の連鎖から配列リテラルを使った実装に変更されました。
変更前:
def hash
Column.hash ^
name.hash ^
name.encoding.hash ^
cast_type.hash ^
default.hash ^
sql_type_metadata.hash ^
null.hash ^
default_function.hash ^
collation.hash ^
comment.hash
end
変更後:
def hash
[
Column,
@name,
@name.encoding,
@cast_type,
@default,
@sql_type_metadata,
@null,
@default_function,
@collation,
@comment,
].hash
end
同様の変更が以下のクラスにも適用されています:
-
ActiveRecord::ConnectionAdapters::SqlTypeMetadata:
sql_type、type、limit、precision、scaleの5つの属性 -
ActiveRecord::ConnectionAdapters::PostgreSQL::Column: 基底クラスのハッシュ値に
identity、serial、virtualを追加 -
ActiveRecord::ConnectionAdapters::PostgreSQL::TypeMetadata: 委譲先オブジェクト、
oid、fmodの組み合わせ -
ActiveRecord::ConnectionAdapters::MySQL::TypeMetadata: 委譲先オブジェクトと
extraの組み合わせ -
ActiveRecord::ConnectionAdapters::SQLite3::Column: 基底クラスのハッシュ値に
auto_increment、rowid、virtualを追加
ActiveRecord::Coreの#hashメソッドも同様に更新されました:
def hash
id = self.id
if self.class.composite_primary_key? ? primary_key_values_present? : id
[self.class, id].hash
else
super
end
end
ActiveRecord::ConnectionAdapters::PostgreSQL::Utils::Nameでは、#to_sメソッドと#==メソッドも合わせて改善されました。partsメソッドを廃止し、インスタンス変数を直接参照することで、メソッド呼び出しのオーバーヘッドを削減しています。
設計判断
sql_type_metadata.rbでは、従来XOR演算で使用されていたビットシフト操作(precision.hash >> 1、scale.hash >> 2)が削除されました。配列リテラルを使った実装では、各要素に対して.hashを呼ぶ必要がなく、[a, b, c].hashは内部的に各要素のハッシュ値を計算し、それらを組み合わせます。そのため、Column.hashではなくクラスオブジェクト自体(Column)を配列に含めることで、同じ効果が得られます。
PostgreSQL::Utils::Nameでは、partsというprotectedメソッドを廃止し、@schemaと@identifierを直接参照する実装に変更されました。#to_sメソッドは[@schema, @identifier].compact.join(SEPARATOR)に、#==メソッドは各インスタンス変数の直接比較に、#hashメソッドは配列リテラルを使った実装に変更されています。
まとめ
本PRは、Ruby 3.2の最適化を活用してRails内部のハッシュ値計算を改善した変更です。XOR演算の連鎖から配列リテラルへの切り替えにより、コードの可読性を維持しながら、配列オブジェクトの割り当てを回避するパフォーマンス最適化を実現しています。この変更は、Ruby VMレベルの最適化を適切に活用した好例といえます。