1.1. The Goal: Eliminate False Sharing The objective is to provide utilities that ensure data structures are isolated to their own cache lines, preventing false sharing in concurrent algorithms. False sharing occurs when unrelated data on the same cache line causes unnecessary cache invalidations across CPU cores.
1.2. The Architectural Pattern: Multi-Strategy Padding
- Static Padding: Compile-time alignment and padding using architecture-tuned cache line size.
- Auto Padding: Runtime-detected padding (size only) for dynamic scenarios.
- NUMA Padding: Double cache-line isolation for NUMA architectures.
- Atomic Wrapper: Pre-padded atomic integers for common concurrent patterns.
1.3. The Core Components
- CachePadded.Static: Compile-time padded wrapper with alignment.
- CachePadded.Auto: Runtime-padded wrapper (requires allocator).
- CachePadded.Numa: Double cache-line padding for NUMA.
- CachePadded.Atomic: Cache-line isolated atomic integer.
1.4. Performance Guarantees
- Zero Runtime Cost (Static): All padding computed at compile-time.
- Architecture-Aware: Uses
std.atomic.cache_lineas source of truth. - Power-of-Two Validation: Compile-time check prevents invalid line sizes.
2.1. std.atomic.cache_line as Source of Truth
- Decision: Use Zig's built-in
std.atomic.cache_linefor the architectural cache line size. - Justification: Provides consistent, compiler-verified values across platforms without runtime overhead.
2.2. Alignment + Padding (Static)
- Decision: Static types use both
align(LINE)on the value AND trailing padding bytes. - Justification: Alignment ensures the struct starts on a cache line boundary. Padding ensures the next element in an array doesn't share the same cache line.
2.3. Padding-Only for Auto
- Decision: Auto type adds padding bytes but does NOT change alignment.
- Justification: Runtime-determined alignment isn't possible in Zig's type system. Users needing strict array isolation should use Static variants.
2.4. Double-Line for NUMA
- Decision: NUMA variants use 2× cache line padding.
- Justification: NUMA systems may have adjacent cache lines prefetched together. Double padding prevents cross-socket interference.
2.5. Integer-Only for Atomic
- Decision: Atomic wrapper only accepts integer types (compile-time check).
- Justification:
fetchAdd/fetchSubsemantics are only defined for integers. Other types should use Static wrapper with manual atomic operations.
2.6. Thread-Local Cached Line Size
- Decision: Runtime-detected line size is cached in thread-local storage.
- Justification: Avoids repeated sysfs reads on Linux. Thread-local prevents cross-thread races during initialization.
CachePadded (namespace)
├── Static<T> → struct { value: T align(LINE), pad: [N]u8 }
├── StaticWithLine<T,L> → explicit line size override
├── Auto<T> → struct { value: T, pad: []u8 } (runtime alloc)
├── Numa<T> → struct { value: T align(LINE), pad: [2*LINE - sizeof(T)]u8 }
├── NumaWithLine<T,L> → explicit line size override
├── Atomic<T> → struct { atom: atomic.Value(T) align(LINE), pad: [N]u8 }
└── AtomicWithLine<T,L> → explicit line size override
LINE = std.atomic.cache_line (typically 64 bytes)
| Variant | Alignment | Padding | Allocator | Use Case |
|---|---|---|---|---|
Static<T> |
Yes (LINE) | LINE - sizeof(T) | No | Arrays, struct fields |
Auto<T> |
No | Runtime-computed | Yes | Dynamic/unknown line size |
Numa<T> |
Yes (LINE) | 2×LINE - sizeof(T) | No | NUMA-sensitive data |
Atomic<T> |
Yes (LINE) | LINE - sizeof(atomic) | No | Counters, flags |
-
Static<T>.init(value)- Compile-Time Padding:- Compute padding:
LINE - @sizeOf(T)(0 if T >= LINE) - Return struct with aligned value and padding array
- Compute padding:
-
Auto<T>.init(allocator, value)- Runtime Padding:- Get line size via
detectLineSizeRuntime() - Compute needed padding:
line - @sizeOf(T)(0 if T >= line) - If padding needed, allocate byte slice
- Return struct with value and padding slice
- Get line size via
-
Auto<T>.deinit()- Cleanup:- If padding slice is non-empty, free it
-
detectLineSizeRuntime()- Cached Detection:- If
g_cached_line_size != 0, return cached value - Use
archLine()(std.atomic.cache_line) - Cache and return
- If
Memory Layout Comparison:
Without padding (FALSE SHARING):
┌────────────────────────────────────────────────────────────────┐
│ Cache Line 0 (64 bytes) │
│ ┌──────────┬──────────┬──────────┬──────────┐ │
│ │ counter_a│ counter_b│ counter_c│ counter_d│ ← All 4 share! │
│ └──────────┴──────────┴──────────┴──────────┘ │
└────────────────────────────────────────────────────────────────┘
With CachePadded.Static (NO FALSE SHARING):
┌────────────────────────────────────────────────────────────────┐
│ Cache Line 0 │
│ ┌──────────┬──────────────────────────────────────────────────┐│
│ │ counter_a│ padding ││
│ └──────────┴──────────────────────────────────────────────────┘│
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ Cache Line 1 │
│ ┌──────────┬──────────────────────────────────────────────────┐│
│ │ counter_b│ padding ││
│ └──────────┴──────────────────────────────────────────────────┘│
└────────────────────────────────────────────────────────────────┘
Variant Selection Flow:
Need cache-line isolation?
│
v
┌─────────────┐
│ Know line │──no──> Auto<T> (runtime padding)
│ at compile? │
└──────┬──────┘
│yes
v
┌─────────────┐
│ NUMA system │──yes──> Numa<T> (2× padding)
│ concerns? │
└──────┬──────┘
│no
v
┌─────────────┐
│ Integer │──yes──> Atomic<T> (with load/store/fetchAdd)
│ atomic? │
└──────┬──────┘
│no
v
Static<T>
Static vs Auto:
Static<T> Auto<T>
┌────────────────────┐ ┌────────────────────┐
│ value: T align(64) │ │ value: T │
│ pad: [56]u8 │ │ pad: []u8 (heap) │
└────────────────────┘ └────────────────────┘
│ │
│ sizeof = 64 (fixed) │ sizeof = 16 + alloc
│ alignment = 64 │ alignment = @alignOf(T)
v v
Arrays: each element Arrays: may still share
on own cache line cache lines (no align)
4.1. Unit Tests
- Size validation: Verify
@sizeOf(Static<u8>) == 64. - Alignment check: Verify
@alignOf(Static<u8>) == 64. - Padding calc: Verify large T (>= LINE) gets 0 padding.
- Power-of-two: Verify compile error on invalid line sizes.
- Atomic integer-only: Verify compile error on non-integer Atomic.
4.2. Integration Tests
- Array isolation: Verify adjacent Static elements don't share cache lines.
- Auto allocation: Verify init/deinit doesn't leak.
- NUMA double-padding: Verify
@sizeOf(Numa<u8>) == 128.
4.3. Benchmarks
- False sharing test: N threads incrementing adjacent counters with/without padding.
- Throughput comparison: Measure contention reduction with cache padding.
- Memory overhead: Measure padding waste for various T sizes.