Interlocked Operations on Raw Buffers in DirectX
In DirectX compute kernels, thousands of threads execute concurrently
and often need to write to the same memory addresses within raw buffers
(RWByteAddressBuffer). Without synchronization,
simultaneous access causes data races, torn writes, and unpredictable
output. Interlocked operations solve this by performing atomic
read-modify-write transactions directly at the hardware memory
controller or cache level, ensuring that memory updates occur
sequentially and without thread collisions. This article explains how
interlocked operations operate on raw buffers, how the GPU enforces
atomicity, and the best practices for maintaining both thread safety and
high performance.
The Race Condition Problem in Raw Buffers
A raw buffer in High-Level Shader Language (HLSL) is represented as a
ByteAddressBuffer or RWByteAddressBuffer.
These buffers lack structured data layouts; instead, they treat memory
as a contiguous block of bytes accessed via 32-bit aligned byte
offsets.
When multiple compute shader threads attempt to update the same 32-bit address using standard read and write instructions:
- Thread A reads the current value from the buffer.
- Thread B reads the same value before Thread A finishes writing its update.
- Both threads compute their updates independently.
- Both threads write their values back, causing one thread's update to overwrite and discard the other.
This standard read-modify-write cycle is non-atomic. Because GPU wavefronts (or warps) execute in lockstep across SIMD lanes, resource contention on identical byte addresses occurs frequently.
How Hardware Guarantees Atomicity
Interlocked operations eliminate race conditions by converting the multi-step read-modify-write cycle into a single, indivisible hardware transaction.
When an HLSL intrinsic such as InterlockedAdd is invoked
on an RWByteAddressBuffer:
- Memory Controller Arbitration: The request is routed to the GPU’s L2 cache or memory controllers, which house dedicated atomic arithmetic logic units (ALUs).
- Serialization at the Cache Line: If multiple threads target the exact same 32-bit offset, the memory controller serializes the operations at the hardware level.
- Indivisible Execution: The read, computation, and write-back occur in an uninterruptible cycle for each request. No intervening memory access from any other thread can read intermediate data or write a competing value.
Because this synchronization occurs at the memory architecture level, it requires no software locks, operating system semaphores, or thread context switches.
Common HLSL Interlocked Functions on Raw Buffers
HLSL provides a family of interlocked intrinsics tailored for
RWByteAddressBuffer resources. These operations require
byte offsets that are strictly multiples of 4 (32-bit aligned):
InterlockedAdd(dest, value, original_value): Atomically adds a value to the target 32-bit integer.InterlockedMin(dest, value, original_value)andInterlockedMax(...): Compares and updates the target with the minimum or maximum value.InterlockedOr(...),InterlockedAnd(...),InterlockedXor(...): Executes bitwise manipulation atomically.InterlockedExchange(dest, value, original_value): Sets a new value and retrieves the previous value atomically.InterlockedCompareExchange(dest, compare_value, value, original_value): Replaces the target value only if it matchescompare_value. This forms the foundation for lock-free algorithms on the GPU.
In each intrinsic, retrieving the original_value is
optional, but doing so allows threads to obtain a unique index, such as
an allocation slot in a shared global append counter.
Addressing Performance Bottlenecks and Contention
While interlocked operations guarantee correctness, excessive contention can severely degrade compute performance. When thousands of threads attempt atomic operations on the same memory address simultaneously, execution collapses from massive parallelism into sequential execution at the memory controller.
To maintain thread safety while maximizing throughput:
- Use Wave Intrinsics First: Before writing to global
raw buffers, use wave-level operations (such as
WaveActiveSum) to aggregate values across all active lanes in a wave. Only a single lane per wave then needs to execute theInterlockedAddto the raw buffer, cutting global atomic operations by a factor of 32 or 64. - Utilize
groupsharedMemory: For algorithms requiring workgroup-wide coordination, use interlocked operations on fast, on-chipgroupsharedmemory first. Once the threadgroup resolves its local updates, a single thread updates the globalRWByteAddressBuffer. - Minimize Contention Hotspots: Stagger memory offsets or use hashing strategies to distribute atomic operations across multiple cache lines whenever possible.