Rust Performance Engineering

Data-Oriented Design

Data-Oriented Design (DOD) structures a program around its bulk data transformations and memory access patterns rather than around isolated object hierarchies. Instead of grouping hot and cold fields together inside individual heap-allocated objects, Data-Oriented Design separates hot fields into contiguous arrays (Struct of Arrays / SoA), replaces object pointers with compact integer handles (arenas / generational indices), and processes batches of items in tight, vectorizable loops.

1. Array of Structs (`AoS`: `Vec<Entity>` where each `Entity` is 128 bytes):
   [ pos_x | pos_y | vel_x | vel_y | name: String (24B) | inventory: Vec (24B) | ... ]
   ---> Updating `pos += vel` loads 112 bytes of unused cold fields into L1 cache per entity!

2. Struct of Arrays (`SoA`: Parallel contiguous arrays of hot fields):
   `pos_x`: [ x0, x1, x2, x3, x4, x5, x6, x7 ]  (32 bytes = 8 floats per SIMD register!)
   `pos_y`: [ y0, y1, y2, y3, y4, y5, y6, y7 ]
   `vel_x`: [ vx0, vx1, vx2, vx3, vx4, vx5, vx6, vx7 ]
   `vel_y`: [ vy0, vy1, vy2, vy3, vy4, vy5, vy6, vy7 ]
   ---> 100% of every fetched cache line is used; auto-vectorizes into `vfmadd` SIMD!

Comparing Array of Structs (AoS) and Struct of Arrays (SoA)

Pattern Layout Best Used When
Array of Structs (AoS) (Vec<Particle>) All fields of a single item sit adjacent in memory. Operations randomly look up a single item by index and read all of its fields at once.
Struct of Arrays (SoA) (ParticlesSoA) Each field is stored in its own parallel Vec<Field>. Batch loops iterate over thousands of items touching only 1–3 hot fields (physics, analytics, columnar query engines).
Hot/Cold Splitting Vec<HotBody> + Vec<Box<ColdMetadata>> Keeps the AoS struct tiny (e.g., 16–32 bytes) by moving rarely accessed debug/string fields into a side table.

Implementing a Struct-of-Arrays (SoA) Simulation Step

pub struct ParticlesSoA {
    pub pos_x: Vec<f32>,
    pub pos_y: Vec<f32>,
    pub vel_x: Vec<f32>,
    pub vel_y: Vec<f32>,
    // Cold metadata kept completely out of the physics cache lines:
    pub debug_labels: Vec<String>,
}

impl ParticlesSoA {
    pub fn with_capacity(cap: usize) -> Self {
        Self {
            pos_x: Vec::with_capacity(cap),
            pos_y: Vec::with_capacity(cap),
            vel_x: Vec::with_capacity(cap),
            vel_y: Vec::with_capacity(cap),
            debug_labels: Vec::with_capacity(cap),
        }
    }

    pub fn spawn(&mut self, px: f32, py: f32, vx: f32, vy: f32, label: impl Into<String>) {
        self.pos_x.push(px);
        self.pos_y.push(py);
        self.vel_x.push(vx);
        self.vel_y.push(vy);
        self.debug_labels.push(label.into());
    }

    /// Updates all particle positions in two tight, contiguous SIMD-friendly loops
    /// without ever touching `debug_labels` in memory!
    pub fn step_physics(&mut self, dt: f32) {
        for (px, &vx) in self.pos_x.iter_mut().zip(self.vel_x.iter()) {
            *px = vx.mul_add(dt, *px);
        }
        for (py, &vy) in self.pos_y.iter_mut().zip(self.vel_y.iter()) {
            *py = vy.mul_add(dt, *py);
        }
    }
}

fn main() {
    let mut world = ParticlesSoA::with_capacity(4);
    world.spawn(0.0, 0.0, 10.0, 5.0, "p0");
    world.spawn(1.0, 2.0, -2.0, 4.0, "p1");

    world.step_physics(0.5);

    assert_eq!(world.pos_x, vec![5.0, 0.0]);
    assert_eq!(world.pos_y, vec![2.5, 4.0]);
    println!("Updated SoA positions: x={:?}, y={:?}", world.pos_x, world.pos_y);
}
Updated SoA positions: x=[5.0, 0.0], y=[2.5, 4.0]

Replacing Self-Referential Pointers with u32 Arena Indices

In Rust, graphs and trees built with Rc<RefCell<Node>> suffer from both borrow-checker friction and pointer-chasing cache misses (plus 8-byte pointers on 64-bit platforms). Storing all nodes in a flat Vec<Node> and referencing children via #[repr(transparent)] struct NodeId(u32) cuts reference size in half (4 bytes instead of 8 bytes), keeps all nodes contiguous in cache, and satisfies the borrow checker with zero RefCell overhead.