メモリレイアウトの最適化: Array of Structs (AoS) vs. Struct of Arrays (SoA)

メモリレイアウトはアプリケーションのパフォーマンスに大きな影響を与えます。なぜなら、CPUキャッシュの効率は、漸近的なアルゴリズムの計算量よりも重要であることが多いためです。データをハードウェアのキャッシュラインサイズ(通常は64バイト)に合わせて構造化することで、開発者はメモリのストールとレイテンシを劇的に削減できます。

CPUキャッシュの仕組みとレイテンシ

現代のCPUはメモリから単一のバイトを読み取るのではなく、キャッシュラインと呼ばれるチャンク単位でデータをフェッチします。単一のバイトにアクセスすると、ハードウェアは近くのデータもすぐに必要になると予測して(空間的および時間的局所性)、周囲の64バイトをキャッシュラインとしてキャッシュに充填します。

メモリへのアクセスレイテンシは、データがどこにあるかによって桁違いに異なります。

キャッシュレベル サイズ (コアあたり概算) レイテンシ (サイクル) レイテンシ (時間)
L1d Cache ~35 KiB 4-5 cycles 1-2 ns
L2 Cache ~2 MiB 12-15 cycles 4-5 ns
L3 Cache 12 MiB (shared) 30-40 cycles 10-15 ns
DRAM N/A 100-200 cycles 60-100 ns

Array of Structs (AoS) vs. Struct of Arrays (SoA)

メモリ内でデータがどのように整理されているかは、単一のフェッチ中にキャッシュラインにどれだけの有用な情報がロードされるかを決定します。

Array of Structs (AoS)

AoSレイアウトでは、単一のオブジェクトのすべてのフィールドが連続して格納されます。例えば、ID、座標、体力、および is_alive フラグを含む Monster 構造体は64バイトを占有するかもしれません。プログラムがこれらのモンスターの配列を反復処理して、生存しているものだけをフィルタリングする場合、CPUはモンスターごとに64バイトのキャッシュラインをフェッチします。is_alive の1バイトしか使用されない一方で、他の63バイトは不要にキャッシュにロードされます。

Struct of Arrays (SoA)

SoAレイアウトでは、各フィールドは個別の配列に格納されます。同じ Monster の例を使用すると、すべてのモンスターの is_alive フラグが連続して詰め込まれます。単一の64バイトのキャッシュラインフェッチにより、同時に64個の異なるモンスターの is_alive ステータスを取得できます。このアプローチは、特に個々のオブジェクトのサイズが増大するにつれて、最大30倍のパフォーマンス向上をもたらす可能性があります。

ランダムアクセスパターンへの影響

シーケンシャルアクセスはCPUのプリフェッチャの恩恵を受けますが、ハッシュマップ、ツリー、またはポインタを多用する構造体などのランダムアクセスパターンは予測不可能です。これらの場合、CPUはデータをプリフェッチできず、パフォーマンスはキャッシュサイズに対するワーキングセットの総サイズによって決まります。

ワーキングセットが特定のキャッシュレベルの容量を超えると、CPUはより低速なレベルからフェッチしなければならず(例:L1からL2へのスピル)、レイテンシの「階段状」の増加を引き起こします。例えば、512個のモンスターがいる場合、64Bの構造体はL1d (3 ns) に収まりますが、構造体のサイズを128Bに倍増させると、データはL2 (11 ns) に押し出されます。

技術的な洞察とトレードオフ

業界の実務家は、データ指向設計 (SoA) は強力ですが、AoSの普遍的な代替手段ではないことを強調しています。最適な選択は、アクセスパターンに完全に依存します。

主な検討事項:

  • アクセスパターン: SoAは、多くのオブジェクトにわたって単一のフィールドをフィルタリングまたは更新する場合に優れています。AoSは、プログラムが単一のオブジェクトのほとんどのフィールドに一度にアクセスする必要がある場合に、しばしばより効率的です。

  • 言語のオーバーヘッド: Javaのような高レベル言語は、オブジェクトヘッダー(通常は12バイト、ただし新しいJVMリリースでは8バイトに減少)を導入します。これはすべてのオブジェクトに追加のオーバーヘッドとなり、メモリレイアウトを複雑にする可能性があります。Project Valhalla は、これらを解決するために、特定のケースでヘッダーを排除するためのツールを提供することを目指しています。

  • 開発コスト: 高度に最適化されたビットフィールドやSoAレイアウトを設計することは、単にクラスにフィールドを追加するよりも多くのエンジニアリング時間を要します。開発者は、マイクロ最適化の必要性と開発速度のバランスを取る必要があります。

  • SIMD の潜在能力: SoAに移行することで、SIMD (Single Instruction, Multiple Data) 命令を使用してさらなる最適化が可能になり、CPUが単一のサイクルで複数のデータポイント(例:ビットマスク)を処理できるようになります。

"SoAは大きな勝利をもたらす可能性があります。しかし、単純なAoSも同様であり、それはアクセスパターンに依存します。重要なワークロードのプロファイリングが重要です。それなしでは、他のすべては推測に過ぎません。"

最終的に、最も効果的な戦略は、非オブジェクト指向のデータ構造の複雑さに取り組む前に、コードのホットスポットをプロファイリングして、メモリレイアウトが主要なボトルネックであるかどうかを判断することです。

Sources