Course contentsShow
← Rust
Lesson 1769 of 1,91015. Performance and Architectural EngineeringFree lesson

Designing Cache-Friendly Work Queues

Align work queues to cache lines to prevent ping-ponging across processor cores.

In this lesson, you will learn how to structure work queues to avoid "false sharing" and cache-line bouncing, ensuring your high-throughput Rust applications stay performant.

The Problem: False Sharing

When multiple processor cores operate on data residing on the same cache line, the CPU’s cache coherence protocol (like MESI) forces the line to be invalidated and re-fetched across cores whenever any core modifies its portion of that data. This is known as "cache ping-ponging." In a work queue, if your head (producer index) and tail (consumer index) reside on the same 64-byte cache line, the producer and consumer will constantly fight for ownership of that line, even if they aren't accessing the same memory addresses.

Designing Cache-Friendly Structures

To prevent this, we must ensure that high-frequency write variables reside on different cache lines. In Rust, we achieve this by using the #[repr(align(64))] attribute on struct fields.

By padding these fields, we force the compiler to place them on separate memory boundaries.

#[repr(align(64))]
struct CachePadded<T> {
    value: T,
}

pub struct WorkQueue<T> {
    // Producer modifies this frequently
    head: CachePadded<AtomicUsize>,
    
    // Consumer modifies this frequently
    tail: CachePadded<AtomicUsize>,
    
    // The actual storage
    buffer: Vec<T>,
}

In this layout, the head and tail pointers are guaranteed to exist on distinct cache lines. Even if a producer increments head and a consumer increments tail at the exact same time, the hardware cache controllers will not need to invalidate the entire line for the other core, as each pointer occupies its own private cache line.

Strategic Placement

When designing these queues, consider the access pattern. If you are building a Multi-Producer, Single-Consumer (MPSC) queue, you only need to worry about isolating the producer index from the consumer index. If you have multiple producers, you would need to go further, perhaps using thread-local queues to minimize contention on the global head index entirely. Always keep your frequently mutated indices "spread out" to allow the CPU to manage cache state without inter-core synchronization overhead.

Key Takeaway: By using #[repr(align(64))] to separate hot, mutable indices into different cache lines, you eliminate false sharing and prevent cache-line ping-ponging, allowing your work queues to scale linearly with your CPU cores.