Memory Layout and Representation

Alignment and Performance

Alignment and performance are directly coupled in modern CPU architectures because memory subsystems transfer data in fixed-size cache lines (typically 64 bytes on x86_64 and 64 or 128 bytes on AArch64) and execute wide SIMD instructions that operate on 16-, 32-, or 64-byte aligned registers.

Hardware Effects of Alignment

  1. Single-instruction aligned loads: When an 8-byte u64 lives at an address divisible by 8, it is guaranteed never to straddle two cache lines or two virtual memory pages. The CPU fetches it in a single cache transaction.
  2. Cache-line splits on unaligned accesses: An unaligned u64 (#[repr(packed)] or ptr.read_unaligned()) that crosses a 64-byte boundary forces the CPU load-store unit to issue two separate cache-line reads and shift/merge the bytes in hardware. On strict-alignment architectures (such as Cortex-M0 or older ARM targets), unaligned hardware load instructions trap with an alignment fault.
  3. Atomics requirement: Hardware atomic instructions (AtomicU64, AtomicUsize) require natural alignment; unaligned atomic operations either fault in hardware or fall back to global spinlocks.

False Sharing and Cache-Line Alignment (#[repr(align(64))])

When two threads concurrently modify independent variables that happen to sit on the same 64-byte cache line, the CPU cache-coherency protocol (MESI/MOESI) repeatedly invalidates the entire cache line between cores. This performance degradation is called false sharing.

Raising a per-thread state struct's alignment with #[repr(align(64))] pads its size to a multiple of 64 bytes, guaranteeing that adjacent elements in an array [PaddedWorker; N] occupy distinct cache lines.

Without #[repr(align(64))] — False Sharing (Both counters in one 64B cache line):
|<-------------------------- 64-Byte Cache Line -------------------------->|
+------------------+------------------+------------------------------------+
| Core 0: count_a  | Core 1: count_b  |         other stack/heap data      |
|    (8 bytes)     |    (8 bytes)     |             (48 bytes)             |
+------------------+------------------+------------------------------------+

With #[repr(align(64))] — Isolated Cache Lines:
|<-------------------------- Cache Line 0 (64B) -------------------------->|
+------------------+-------------------------------------------------------+
| Core 0: count_a  |                  56 bytes padding                     |
+------------------+-------------------------------------------------------+
|<-------------------------- Cache Line 1 (64B) -------------------------->|
+------------------+-------------------------------------------------------+
| Core 1: count_b  |                  56 bytes padding                     |
+------------------+-------------------------------------------------------+
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;

#[repr(align(64))]
struct CachePaddedAtomic {
    value: AtomicU64,
}

fn main() {
    let counters = [
        CachePaddedAtomic {
            value: AtomicU64::new(0),
        },
        CachePaddedAtomic {
            value: AtomicU64::new(0),
        },
    ];

    println!(
        "size of CachePaddedAtomic:  {} bytes",
        size_of::<CachePaddedAtomic>()
    );
    println!(
        "align of CachePaddedAtomic: {} bytes",
        align_of::<CachePaddedAtomic>()
    );
    println!(
        "distance between counters:  {} bytes",
        (&counters[1] as *const _ as usize) - (&counters[0] as *const _ as usize)
    );

    thread::scope(|s| {
        for counter in &counters {
            s.spawn(|| {
                for _ in 0..10_000 {
                    counter.value.fetch_add(1, Ordering::Relaxed);
                }
            });
        }
    });

    println!(
        "totals: [{}, {}]",
        counters[0].value.load(Ordering::Relaxed),
        counters[1].value.load(Ordering::Relaxed)
    );
}
size of CachePaddedAtomic:  64 bytes
align of CachePaddedAtomic: 64 bytes
distance between counters:  64 bytes
totals: [10000, 10000]

SIMD Buffer Alignment

Vectorized loops (SSE2 16-byte, AVX2 32-byte, AVX-512 64-byte) achieve peak throughput when buffers are aligned to the vector register width. Wrapping an array in a #[repr(C, align(32))] struct guarantees 32-byte alignment on both the stack and the heap.

#[repr(C, align(32))]
struct SimdBlock {
    lanes: [f32; 8],
}

fn add_blocks(a: &SimdBlock, b: &SimdBlock) -> SimdBlock {
    let mut out = SimdBlock { lanes: [0.0; 8] };
    for i in 0..8 {
        out.lanes[i] = a.lanes[i] + b.lanes[i];
    }
    out
}

fn main() {
    let a = SimdBlock {
        lanes: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
    };
    let b = SimdBlock {
        lanes: [10.0; 8],
    };
    let sum = add_blocks(&a, &b);

    println!("SimdBlock size: {}, align: {}", size_of::<SimdBlock>(), align_of::<SimdBlock>());
    println!("address % 32 == {}", (&sum as *const SimdBlock as usize) % 32);
    println!("sum.lanes:      {:?}", sum.lanes);
}
SimdBlock size: 32, align: 32
address % 32 == 0
sum.lanes:      [11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0]

Array-of-Structs (AoS) vs. Struct-of-Arrays (SoA)

When a hot loop only accesses one or two fields of a large struct, an Array of Structs (AoS, Vec<Particle>) pulls unused cold fields and padding bytes into every 64-byte cache line. Splitting fields into a Struct of Arrays (SoA) packs homogeneous values contiguously, maximizing cache-line utilization and enabling automatic SIMD vectorization.

Layout Pattern Structure Cache Utilization on Single-Field Scan
Array of Structs (AoS) struct Particle { pos: [f32; 3], vel: [f32; 3], name_id: u64 } Only 12 of every 32 bytes read (37.5%) are pos
Struct of Arrays (SoA) struct Particles { pos_x: Vec<f32>, pos_y: Vec<f32>, ... } 100% of every 64-byte cache line holds active f32 coordinates