キャッシュヒット時のKeyFormatterパフォーマンスを最適化
Jbuilder 8.1では、KeyFormatterのキャッシュヒット時のパフォーマンスが2.33倍に向上しました。この改善は、check-lock-checkパターンの導入により、ミューテックスのロック頻度を大幅に削減することで実現されています。
背景
JbuilderのKeyFormatterは、キー名を指定されたフォーマット(例::camelcaseや:downcase)に変換する際、変換結果をキャッシュしています。これまでの実装では、キャッシュの読み取り時にも必ずミューテックスをロックしていたため、マルチスレッド環境下で競合が発生し、パフォーマンスのボトルネックとなっていました。
#597で導入された変更を基に、#607では、より効率的なキャッシュアクセスパターンを実装しています。
技術的な変更
変更前:
def format(key)
@mutex.synchronize do
@cache[key] ||= begin
value = key.is_a?(Symbol) ? key.name : key.to_s
# フォーマット処理...
end
end
end
変更後:
def format(key)
@cache[key] || @mutex.synchronize do
@cache[key] ||= begin
value = key.is_a?(Symbol) ? key.name : key.to_s
# フォーマット処理...
end
end
end
この変更により、以下のような動作フローとなります:
パフォーマンスへの影響
ベンチマーク結果では、KeyFormatter#format単体で2.33倍の高速化を達成しています:
- 変更前: 10.27M iterations/sec (97.32 ns/iteration)
- 変更後: 23.93M iterations/sec (41.79 ns/iteration)
実際のJbuilder使用時(json.set!呼び出し)でも1.29倍の改善が見られます:
- 変更前: 4.64M iterations/sec (215.45 ns/iteration)
- 変更後: 5.99M iterations/sec (166.86 ns/iteration)
設計判断
この実装は、Ruby並行処理ライブラリであるconcurrent-rubyのConcurrent::Mapと同様のアプローチを採用しています。check-lock-checkパターンの採用により:
- キャッシュヒット時(最も一般的なケース): ミューテックスのロックが不要となり、高速なパスが実現
- キャッシュミス時: ミューテックスをロックするが、ロック取得後に再度キャッシュを確認することで、他のスレッドが既に計算を完了している場合の無駄な計算を回避
この最適化は、キーフォーマッティングがJbuilderのホットパスの一つであることから、特に高トラフィックなAPIエンドポイントで顕著な効果を発揮します。