捨てるためだけに割り当てた7.8 MBのキー
プロファイラーによって、コード内に潜んでいた隠れたコストが露呈した。
リクエストハンドラーは、巨大な文字列からトークンを読み取り、その重みを合計する。ロジックは正しく動作していたが、メモリ管理が問題だった。
ホットループの中で、辞書のルックアップごとに新しいキーを作成し、合計7.8 MBもの文字列を割り当てては、すぐに破棄していたのだ。
原因は Substring だった。スライスを行うたびに、新しい文字列オブジェクトが生成されていた。
200,000 トークン
- 手法 A (Substring): 23.4 ms、7,812 KB 割り当て。
- 手法 B (Span Lookup): 14.9 ms、0 KB 割り当て。
手法 B は 1.5 倍高速で、ガベージを生成しない。
.NET 9 では GetAlternateLookup を呼び出すことができる。新しい文字列の代わりに ReadOnlySpan<char> を渡す。これは元の文字列を覗き見るための「窓」のようなもので、コピーは発生しない。
辞書は既存のキーに対して Span を直接ハッシュ化するため、メモリ割り当てを行うことなく同じ結果を得られる。
留意事項
StringComparer.OrdinalおよびStringComparer.OrdinalIgnoreCaseで動作する。- Alternate lookup をサポートしていないカスタム比較子を使用している場合、実行時に例外がスローされる。
- パーサー、ログプロセッサ、CSVスキャナーなどのホットパスに最適。
- キーが数個しかない単純なルックアップでは、あえて使う必要はない。
今では、コードの中から Substring の後に TryGetValue が続くパターンを執拗に探している。そのパターンは膨大なメモリを浪費するからだ。
Source: https://dev.to/ssukhpinder/78-mb-of-keys-i-allocated-just-to-throw-away-50mn
Optional learning community: https://github.com/ssukhpinder/dev-to-code-samples/tree/main/023-dictionary-alternate-lookup
