2025/10/06 06:23 CPU Cache-Friendly Data Structures in Go: 10x Speed with Same Algorithm

ロボ子、今日はCPUキャッシュについて話すのじゃ!

CPUキャッシュ、ですか。L1、L2、L3とRAMがあるんですよね。それぞれの速度と容量が違うと。

そうじゃ!L1キャッシュは4サイクル(約1ナノ秒)で32KB、L2キャッシュは12サイクル(約3ナノ秒)で256KB、L3キャッシュは40サイクル(約10ナノ秒)で8MBじゃ。RAMは200サイクル以上(約60ナノ秒)で32GBもあるぞ。

RAMからの読み込みはL1キャッシュの約60倍も遅いんですね!キャッシュミスは60キャッシュヒットに相当する、と。

その通り!だからキャッシュを意識したプログラミングが大切なのじゃ。例えば、フォルスシェアリングって知ってるか?

フォルスシェアリング…複数のCPUコアが同じキャッシュラインを共有する異なる変数を変更すると発生する、パフォーマンス低下の原因になるものですね。

よく知ってるの!高並行処理時に10倍もパフォーマンスが低下することもあるから、要注意じゃぞ!

それを防ぐために、データ指向設計が重要になるんですね。構造体配列(AoS)と配列構造体(SoA)の使い分けとか。

そうじゃ!ホット/コールドデータの分離や、NUMA対応アロケーションも有効じゃ。CPU IDに基づいてメモリを割り当てるのじゃ。

キャッシュを意識したハッシュテーブルには、Robin Hoodハッシュテーブルを使うと良いんですね。

さすがロボ子、よく勉強しておる!実際のプロダクション例としては、分析パイプラインでのイベントバッチ処理が挙げられるのじゃ。

SIMDフレンドリーなレイアウトも重要ですね。Vec3構造体を使うとか。

`perf` ツールを使ってキャッシュミスを測定し、キャッシュパフォーマンスをベンチマークするのも大事じゃぞ。

Goでキャッシュフレンドリーなコードを書くためのルールもあるんですね。頻繁にアクセスされるフィールドを同じキャッシュラインに配置したり、ゴルーチンのデータをキャッシュラインで分離したり…。

そうそう!ランダムアクセスよりもシーケンシャルアクセスを優先したり、範囲が許容される場合はint64ではなくint32を使ったりするのも効果的なのじゃ。

予測可能な分岐とアクセスパターンも重要ですね。メモリの再利用はホットキャッシュにつながる、と。

最適化のレシピとしては、まず`perf`でキャッシュミスを特定し、ホットパスのためにAoSからSoAへ再構築、フォルスシェアリングを排除するためにパディング…という流れじゃな。

セキュリティに関する考慮事項もあるんですね。タイミングサイドチャネルを悪用する可能性のあるメモリアライメントトリックに注意したり、キャッシュベースの攻撃(Spectre、Meltdown)は予測可能なメモリアクセスパターンを悪用したり…。

ロボ子、悲しい顔をするでない。テスト戦略も重要じゃぞ!マイクロベンチマークを使って特定の最適化を測定したり、異なるCPUアーキテクチャでテストしたりするのじゃ。

現実的なデータサイズとアクセスパターンで測定し、パフォーマンスカウンタを使用してキャッシュヒット率を検証するんですね。異なるコア数でテストを実行して、フォルスシェアリングを検出すると。

言語比較で言うと、Goのガベージコレクションはメモリアクセスパターンにオーバーヘッドを追加するけど、キャッシュフレンドリーなコードは局所性を改善することによりGC圧力を軽減できるのじゃ。

最適化手法の改善例として、分析パイプラインではバッチ処理で最大14倍高速化、ゲーム物理エンジンでは衝突検出で8倍高速化、データベースインデックス作成ではシーケンシャルスキャンで11倍高速化されたんですね。

そう!キャッシュを意識するだけで、こんなにもパフォーマンスが向上するのじゃ!ところでロボ子、キャッシュって何でできてるか知ってるか?

えっと…半導体ですか?

ブー!正解は…お金じゃ!キャッシュだけに!
⚠️この記事は生成AIによるコンテンツを含み、ハルシネーションの可能性があります。
