Sequential Consistency vs Relaxed Memory Ordering in C

This article examines the core differences between sequential consistency (memory_order_seq_cst) and relaxed memory ordering (memory_order_relaxed) in C low-level concurrency via <stdatomic.h>. It explores how each model influences compiler optimizations and hardware instruction reordering, the operational guarantees each provides, and the practical performance trade-offs involved when building concurrent algorithms.

Understanding the C11 Memory Model

Introduced in the C11 standard, the <stdatomic.h> library provides explicit memory ordering semantics to control how reads and writes to atomic variables propagate across processor cores. In modern multi-core architectures, compilers and hardware dynamically reorder instructions and utilize store buffers to maximize execution throughput. Atomic memory orders tell both the compiler and the CPU which reorderings are strictly forbidden and which are permissible.

Sequential Consistency (memory_order_seq_cst)

Sequential consistency is the default memory order applied when using atomic operations without specifying an explicit order in C (such as atomic_store instead of atomic_store_explicit).

  1. Global Order: Sequential consistency guarantees that all threads observe all seq_cst operations in a single, universally agreed-upon total order. If Thread A stores to variable X and then to variable Y, no thread in the system can observe the write to Y before the write to X.
  2. Synchronization and Ordering: Every sequentially consistent operation acts as both an acquire and release barrier, preventing surrounding reads and writes from being reordered before or after the atomic operation.
  3. Overhead: Maintaining a globally coherent timeline requires hardware synchronization primitives, such as full memory fences (e.g., MFENCE on x86) or locked instructions. This creates significant latency on architectures with relaxed hardware memory models, such as ARM or POWER.

Relaxed Memory Ordering (memory_order_relaxed)

Relaxed memory ordering represents the weakest synchronization model provided by C.

  1. Atomicity Only: An operation executed with memory_order_relaxed guarantees only that the read or write itself is atomic and will not produce torn reads or data races on that specific variable. It provides no synchronization with other memory operations.
  2. Permissible Reordering: The compiler and the processor are free to reorder relaxed memory operations across other memory accesses (both non-atomic and other relaxed atomics), provided intra-thread program logic (program order) is preserved.
  3. No Global Timeline: Different threads may observe relaxed modifications to distinct variables in completely different orders. If Thread A writes to X and then Y using memory_order_relaxed, Thread B may observe the modification of Y before X.
  4. Performance: Because it eliminates hardware memory barriers, relaxed ordering translates to normal load and store instructions on almost all architectures, offering minimal execution overhead.

Key Differences

  • Inter-Thread Synchronization: memory_order_seq_cst creates a clear "happens-before" relationship across threads, enabling flag-based coordination and synchronization of non-atomic payloads. memory_order_relaxed cannot be used to coordinate access to other shared state, as it does not enforce ordering constraints relative to other memory locations.
  • Visibility: Sequentially consistent operations establish a single global modification order visible to all CPU cores. Relaxed operations do not synchronize caches; operations become visible to other threads only as cache coherency protocols dictate, without timing or sequencing guarantees relative to other operations.
  • Cost: Sequential consistency enforces conservative synchronization, preventing CPU pipelines from executing out-of-order memory operations. Relaxed ordering allows hardware to optimize instruction pipelines and memory buses freely.

Practical Application

Use memory_order_seq_cst when correctness requires a deterministic, global timeline across multiple shared variables and performance is secondary to preventing subtle concurrency bugs.

Use memory_order_relaxed exclusively when an operation does not guard or depend on any other state. Common use cases include monotonic performance counters, statistical metrics, or atomic reference counter increments where the sequencing relative to other data is irrelevant to program correctness.