Low-Level Systems Programming

Cache Lines and False Sharing

Modern CPUs do not transfer individual bytes or 8-byte words between RAM and L1/L2/L3 caches. Instead, cache hardware transfers memory in fixed-size contiguous blocks called cache lines—typically 64 bytes on x86_64 and Linux ARM64, or 128 bytes on Apple Silicon (M1/M2/M3/M4) and IBM POWER.

False sharing occurs when two independent variables (such as worker_0_count: AtomicU64 and worker_1_count: AtomicU64) reside inside the exact same cache line. Even though Thread 0 only modifies worker_0_count and Thread 1 only modifies worker_1_count, the CPU's cache coherency protocol (MESI / MOESI) must invalidate and bounce the entire 64/128-byte cache line back and forth between the two cores on every write, degrading throughput by up to 10x–50x!

False Sharing (Both Atomics Sit in the Same 64-Byte Cache Line!):
  +-------------------------------------------------------------------+
  | Cache Line 0 (64 Bytes)                                           |
  | [ worker_0: AtomicU64 (8B) ] [ worker_1: AtomicU64 (8B) ] [ 48B ] |
  +-------------------------------------------------------------------+
         ^                              ^
         | Core 0 writes                | Core 1 writes
         +--- Cache Line Ping-Pongs! ---+ (Massive L1 invalidation stalls)

Cache-Line Padded (`#[repr(align(64))]` or `#[repr(align(128))]`):
  +-----------------------------------+   +-----------------------------------+
  | Cache Line 0 (64B / 128B)         |   | Cache Line 1 (64B / 128B)         |
  | [ worker_0: AtomicU64 ] + padding |   | [ worker_1: AtomicU64 ] + padding |
  +-----------------------------------+   +-----------------------------------+
         ^                                       ^
         | Exclusive to Core 0 L1 Cache          | Exclusive to Core 1 L1 Cache

Eliminating False Sharing with #[repr(align(...))]

Wrapping frequently mutated per-thread or per-shard atomics inside a #[repr(align(128))] (or #[repr(align(64))]) wrapper struct guarantees that each instance starts on its own cache line boundary and is padded to a full cache line multiple:

use std::mem::{align_of, size_of};
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;

/// Aligns and pads `T` to 128 bytes (covering both 64B x86_64 and 128B Apple Silicon/ARM64 cache lines).
#[repr(align(128))]
#[derive(Debug, Default)]
pub struct CachePadded<T> {
    pub value: T,
}

impl<T> CachePadded<T> {
    pub const fn new(value: T) -> Self {
        Self { value }
    }
}

impl<T> Deref for CachePadded<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

impl<T> DerefMut for CachePadded<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.value
    }
}

pub struct ShardedCounter {
    shards: [CachePadded<AtomicU64>; 4],
}

impl ShardedCounter {
    pub const fn new() -> Self {
        Self {
            shards: [
                CachePadded::new(AtomicU64::new(0)),
                CachePadded::new(AtomicU64::new(0)),
                CachePadded::new(AtomicU64::new(0)),
                CachePadded::new(AtomicU64::new(0)),
            ],
        }
    }

    pub fn add(&self, shard_idx: usize, delta: u64) {
        self.shards[shard_idx & 3].fetch_add(delta, Ordering::Relaxed);
    }

    pub fn total(&self) -> u64 {
        self.shards
            .iter()
            .map(|s| s.load(Ordering::Relaxed))
            .sum()
    }
}

fn main() {
    println!(
        "CachePadded<AtomicU64>: size={}B, align={}B",
        size_of::<CachePadded<AtomicU64>>(),
        align_of::<CachePadded<AtomicU64>>()
    );

    let counter = ShardedCounter::new();
    thread::scope(|s| {
        for worker_id in 0..4 {
            let counter_ref = &counter;
            s.spawn(move || {
                for _ in 0..1_000 {
                    counter_ref.add(worker_id, 1);
                }
            });
        }
    });

    assert_eq!(counter.total(), 4_000);
    println!("Total across 4 cache-isolated shards: {}", counter.total());
}
CachePadded<AtomicU64>: size=128B, align=128B
Total across 4 cache-isolated shards: 4000

Grouping Hot Read-Only Fields vs Contended Mutable Fields

Field Access Pattern Optimal Struct Layout Strategy
Read-mostly configuration fields (capacity, mask, hash_seed) Pack together on the same cache line so a single L1 cache fetch loads all of them at once (true sharing).
Frequently written atomic counters/locks (head_idx, tail_idx) Separate onto distinct cache lines with CachePadded<T> so producer and consumer threads never invalidate each other's L1 cache line.