Structure of Arrays vs Array of Structures in Games

Memory layout significantly influences runtime performance in modern game development, where CPU cache efficiency often dictates frame rates more than raw clock speeds. The choice between Array-of-Structures (AoS) and Structure-of-Arrays (SoA) determines how entity data maps to system memory, directly affecting cache hit rates, memory bandwidth utilization, and SIMD (Single Instruction, Multiple Data) vectorization efficiency. While AoS is the intuitive default of object-oriented programming, modern data-oriented game architectures favor SoA patterns to eliminate memory access bottlenecks during intensive batch-processing operations.

Understanding AoS vs. SoA

In an Array-of-Structures layout, each entity is encapsulated as a single struct, and all entities are stored in a contiguous array:

// Array of Structures (AoS)
struct Particle {
    float x, y, z;
    float vx, vy, vz;
    int life;
};
Particle particles[1000];

In a Structure-of-Arrays layout, the entity's data fields are split into separate parallel arrays:

// Structure of Arrays (SoA)
struct ParticleSystem {
    float x[1000], y[1000], z[1000];
    float vx[1000], vy[1000], vz[1000];
    int life[1000];
};

CPU Cache Lines and Spatial Locality

Modern CPUs read data from main memory into L1/L2/L3 caches in discrete chunks, typically 64 bytes wide, known as cache lines.

When a game system iterates over all entities to update only a subset of their attributes—such as updating positions using velocities in a physics pass—the AoS approach suffers from poor spatial locality. As the CPU loads an entity's position and velocity into a cache line, it also loads unrelated fields (such as health, mesh IDs, or audio triggers) residing in the same struct. These unused bytes consume valuable cache space and memory bus bandwidth, leading to frequent cache evictions and cache misses.

Conversely, the SoA approach ensures perfect spatial locality for single-component operations. When updating positions, only position arrays are loaded into the cache lines. Every single byte pulled from main memory into the cache is actively utilized, maximizing cache capacity and drastically reducing memory stalls.

SIMD and Auto-Vectorization

SIMD architectures (such as x86 SSE/AVX or ARM NEON) allow a CPU to execute a single mathematical operation across multiple data points simultaneously—for example, adding four or eight floats in a single cycle.

  • Under AoS: Vectorizing operations across multiple entities requires complex loading and unpacking (swizzling) instructions to isolate the targeted variables into SIMD registers. This unpacking overhead often negates the speedup provided by vector units.
  • Under SoA: Contiguous memory layouts allow four, eight, or sixteen adjacent values (e.g., consecutive x coordinates) to be loaded directly into a SIMD vector register in a single instruction without rearrangement. This makes SoA substantially faster for systems executing heavy math routines across thousands of objects, such as particle updates, skeletal animation, and rigid body integration.

Random Access and Mutation Trade-offs

Despite the batch-processing advantages of SoA, it is not universally superior for every game scenario:

  1. Single-Entity Access: If gameplay logic requires accessing all properties of a single entity at once (e.g., an RPG character taking damage, altering health, triggering an animation state, and reading armor values), AoS keeps those fields close together, requiring only one or two cache line loads. Under SoA, reading all properties of one entity incurs separate memory lookups across multiple distant arrays.
  2. Entity Lifecycle Management: Adding or removing entities dynamically in an SoA layout requires shifting elements across every parallel array, whereas AoS requires updating only a single array.

The Hybrid Solution: AoSoA

To balance the vectorization benefits of SoA with the manageable allocation sizes of AoS, many performance-critical game engines employ an Array of Structures of Arrays (AoSoA), also called tiled or packetized SoA:

struct ParticleBlock {
    float x[8], y[8], z[8];
    float vx[8], vy[8], vz[8];
    int life[8];
};
ParticleBlock blocks[125]; // 1000 particles total

AoSoA groups data into chunks sized specifically to match the native SIMD width of the target processor. This provides optimal SIMD lane utilization while preventing the cache-thrashing that can occur when an algorithm must jump between numerous distant memory streams.

Architectural Impact

The memory access advantages of SoA have driven the adoption of Data-Oriented Design (DOD) and Entity Component Systems (ECS) across the game industry. By separating entity data into isolated, contiguous component arrays, modern engines achieve order-of-magnitude performance gains simply by aligning software architecture with the physical realities of CPU hardware and memory hierarchies.