C++ from Basics to Mastery — Part 22
Design memory cost with data layout, caches, and pmr
The articles on this blog are written for teaching purposes. The information may contain mistakes, so verify it against your target compiler and standard library.
Performance is not determined by operation count alone. Object layout, access order, and allocation frequency affect cache behavior and throughput.
Contiguous data is cache-friendly
struct Particle
{
float X, Y, Z;
float VelocityX, VelocityY, VelocityZ;
bool Active;
};
std::vector<Particle> particles;
An Array of Structures is simple and works well when each pass uses most fields. A pass that updates only positions may still pull unused fields into cache lines.
struct ParticlePositions
{
std::vector<float> X;
std::vector<float> Y;
std::vector<float> Z;
};
A Structure of Arrays keeps equal fields contiguous, which can help bulk work and SIMD. It can make object-oriented APIs and operations over one complete particle more complicated.
Design allocation around lifetime
#include <array>
#include <memory_resource>
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 containers receive their memory resource at run time. A monotonic_buffer_resource allocates forward and releases everything together, which suits temporary data with one shared lifetime.
The resource must outlive every container and element using it. Destroying the arena first leaves those objects referring to invalid storage.
reserve is a reallocation policy
std::vector<Entity> entities;
entities.reserve(expectedCount);
When an expected size is known, reserve can avoid repeated allocation and movement. A large overestimate keeps unused memory committed. Reallocation can also invalidate addresses and iterators, so long-lived external references may require indices or generation-checked handles.
Do not change layout without measurement. Compare representative workloads using wall time, CPU time, allocation count, peak memory, and cache misses while rechecking correctness.
Good memory design is not the cleverest allocator. It makes lifetime and access patterns explicit and uses the simplest ownership model that meets the measured target.