ActiveSupport::Cache のキー正規化に文字列ファストパスを追加
ActiveSupport::Cache のキー正規化処理に、文字列キー向けのファストパスが追加されました。大多数を占める文字列キーに対して #expanded_key の処理をスキップすることで、キャッシュ操作全体のオーバーヘッドを削減します。
背景
キャッシュキーの正規化処理は、あらゆるキャッシュ操作で必ず通るホットパスです。expand_and_namespace_key は内部で #expanded_key を呼び出し、配列・Hashなど任意のオブジェクトを文字列へ変換する汎用処理を実行していました。しかし実際のアプリケーションでは、キャッシュキーの大多数はすでに文字列として渡されるため、この汎用処理は不要なオーバーヘッドになっていました。
PR の説明によれば、「圧倒的多数のキーはすでに文字列」であることが、今回の最適化の根拠です。
技術的な変更
cache.rb:文字列キーのファストパス追加
expand_and_namespace_key の冒頭に型チェックを追加することで、文字列キーは #expanded_key を完全にバイパスするようになりました。
変更前:
def expand_and_namespace_key(key, options = nil)
str_key = expanded_key(key)
変更後:
def expand_and_namespace_key(key, options = nil)
str_key = key.class == ::String ? key : expanded_key(key)
key.is_a?(String) ではなく key.class == ::String を使っている点が特徴的です。is_a? はサブクラスにも true を返しますが、== による直接比較はサブクラスをスキップして純粋な String インスタンスのみをファストパスに誘導します。文字列のサブクラスを使うケースは稀であり、それらは従来通り #expanded_key で処理されます。
mem_cache_store.rb:エンコード処理の簡略化
MemCacheStore の normalize_key も合わせてリファクタリングされました。
変更前:
ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
def normalize_key(key, options)
key = expand_and_namespace_key(key, options)
if key
key = key.dup.force_encoding(Encoding::ASCII_8BIT)
key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
end
truncate_key(key)
end
変更後:
def normalize_key(key, options)
key = expand_and_namespace_key(key, options)
key = key.b
key.gsub!(/[\x00-\x20%\x7F-\xFF]/n) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
truncate_key(key)
end
変更点は3つあります。まず、定数 ESCAPE_KEY_CHARS が削除され、正規表現はインラインで記述されるようになりました。次に key.dup.force_encoding(Encoding::ASCII_8BIT) が key.b に置き換えられました。String#b は force_encoding と異なりコピーを返しつつエンコーディングを ASCII-8BIT に設定するメソッドであり、より慣用的な記法です。さらに gsub が gsub! に変わり、コピーを生成せず破壊的変換を適用することでオブジェクトのアロケーションを1つ減らしています。
また expand_and_namespace_key が nil を返すケースがなくなったため(直前の raise でブランクキーを弾く設計)、if key による nil ガードも不要となり削除されています。
設計判断
is_a? ではなく class == を採用した選択 は、ファストパスの対象を厳密に制御する意図を示しています。ActiveSupport::SafeBuffer など String のサブクラスがキーとして渡された場合に誤った挙動をしないよう、安全側に倒した判断といえます。
MemCacheStore 側の変更は機能的には等価なリファクタリングですが、gsub から gsub! への変更はファストパス追加と同じ方向性—不要なオブジェクト生成の削減—を徹底しています。
まとめ
今回の変更は1行の条件式追加という最小限の変更でありながら、キャッシュキー処理のホットパスにおける不要なメソッド呼び出しとオブジェクト生成を削減します。型チェックの選択(class ==)や破壊的メソッドの活用(gsub!、String#b)に、Rubyにおけるパフォーマンス最適化の典型的なアプローチが凝縮されています。