C++ 基礎からマスターまで — 第22回
データ配置、キャッシュ、pmr でメモリコストを設計する
本ブログの記事は、講義を目的として作成しています。 内容に誤りが含まれる可能性があるため、対象コンパイラと標準ライブラリの資料も確認してください。
性能は演算回数だけでは決まりません。オブジェクト配置、アクセス順序、確保頻度が CPU cache と throughput を変えます。
連続データは Cache を利用しやすい
struct Particle
{
float X, Y, Z;
float VelocityX, VelocityY, VelocityZ;
bool Active;
};
std::vector<Particle> particles;
各処理でほとんどのフィールドを使うなら AoS は単純で局所性も良好です。位置だけ更新する処理では、未使用フィールドまで cache line に入ることがあります。
struct ParticlePositions
{
std::vector<float> X, Y, Z;
};
SoA は同じフィールドを連続配置し、バルク処理と SIMD に有利ですが、一個のオブジェクトを扱う API は複雑になります。
寿命に合わせて allocation を設計する
std::array<std::byte, 4096> buffer;
std::pmr::monotonic_buffer_resource arena(buffer.data(), buffer.size());
std::pmr::vector<std::pmr::string> names(&arena);
names.emplace_back("Knight");
names.emplace_back("Mage");
PMR コンテナは実行時に memory resource を受け取ります。monotonic_buffer_resource は前方へ確保し、最後にまとめて解放するため、同じ短い寿命を持つ一時データに向きます。
Resource は利用するすべてのコンテナと要素より長く生きる必要があります。arena を先に破棄すると無効なメモリを参照します。
reserve は再確保方針
std::vector<Entity> entities;
entities.reserve(expectedCount);
予想サイズが分かれば reserve で再確保と移動を減らせます。過大な予約は未使用メモリを保持します。再確保はアドレスと iterator を無効にするため、長期参照には index や generation 付き handle を検討します。
測定せず layout を変更してはいけません。代表 workload で wall time、CPU time、allocation 数、peak memory、cache miss と正確性を比較します。
良いメモリ設計は最も巧妙な allocator ではありません。寿命とアクセスパターンを明確にし、測定目標を満たす最も単純な所有モデルを使うことです。