Design driven by data layout: SoA instead of AoS, cache lines, batches instead of per-element processing. It runs against encapsulation — a deliberate trade-off for the hot path.
AoS (array of structs) is an array of records, the way a human writes them; SoA (struct of arrays) is one array per field. The difference shows when a loop reads one field across a million records: the CPU pulls memory in 64-byte lines, so AoS drags the whole record into cache and throws away almost the entire line, while SoA places exactly what is being read side by side. Hence the conflict with encapsulation (12): SoA takes the object apart and strips it of the right to hide its representation — which is why it is applied surgically, on a measured (37) hot path, not across the whole codebase.
// until Valhalla, objects mean pointers and cache misses.
// hand-rolled SoA + primitive arrays, or MemorySegment (FFM API)
final class Particles { // instead of List<Particle>
final float[] x, y, vx, vy; // 4 dense arrays, SIMD-friendly
void step(float dt) {
for (int i = 0; i < x.length; i++) { x[i] += vx[i] * dt; y[i] += vy[i] * dt; }
}
}// SoA + iterators compile down to SIMD; #[repr(C)] controls layout
struct Particles { x: Vec<f32>, y: Vec<f32>, vx: Vec<f32>, vy: Vec<f32> }
impl Particles {
fn step(&mut self, dt: f32) {
for (x, vx) in self.x.iter_mut().zip(&self.vx) { *x += vx * dt; }
for (y, vy) in self.y.iter_mut().zip(&self.vy) { *y += vy * dt; }
}
}// same idea — slices of primitives instead of []*Particle
// (removes pointer chasing and GC pressure)
type Particles struct{ X, Y, VX, VY []float32 }# DOD = "get out of the interpreter" — numpy/arrow, vectorize instead of looping
import numpy as np
class Particles:
def __init__(self, n): self.x, self.vx = np.zeros(n), np.zeros(n)
def step(self, dt): self.x += self.vx * dt # one C operation instead of n iterations
# polars/duckdb for tabular data — same principle one level up