AlgoMaster Logo

Memory Ordering, Barriers, and False Sharing

21 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A producer thread fills a response object and then sets ready = true. A consumer waits until ready becomes true and reads the response.

The intended order seems obvious:

The producer writes the response and then publishes ready. The consumer observes ready and then reads the response.

Making ready atomic prevents that flag from tearing. Whether it also publishes the response depends on the ordering semantics used for the store and load; atomicity alone does not answer whether the response writes must become observable before the flag or whether the consumer's response reads must remain after its flag check.

This is a memory-ordering problem. Modern compilers and processors reorder work to improve performance, so concurrent code needs explicit rules connecting operations across threads.

The same hardware creates a second, less obvious problem. Two threads can update completely independent variables and still slow each other down when those variables occupy the same cache line. That performance failure is called false sharing.

Program Order vs. Observed Memory Order

Source code gives each thread a program order:

A person reading the function sees the payload assignment first and the ready assignment second. Three different orders still matter:

  • Source order: the order written by the programmer
  • Execution order: the order in which the compiler and processor carry out work
  • Observation order: the order in which another core can observe the effects

These orders do not always match.

A compiler may move independent operations when doing so does not change the behavior of a valid single-threaded execution. A processor may issue loads early, execute independent instructions out of order, and temporarily hold stores in internal buffers.

These optimizations are essential. Waiting for every memory access to finish globally before beginning the next instruction would waste much of a modern CPU's execution capacity.

The single-threaded result must still appear correct to that thread. The difference becomes visible when another execution path observes shared memory.

Memory ordering defines which observations concurrent execution paths are allowed to make.

A Store Buffer Example

Stores are often placed in a per-core store buffer before they become visible to other cores. This allows the writing core to continue instead of waiting for the cache-coherence work to complete.

Consider two atomic variables initially set to zero:

Two threads use relaxed atomic operations:

Thread AThread B
store x = 1store y = 1
load y into observed_by_aload x into observed_by_b

An execution may behave like this:

StepCore ACore B
1buffer store x = 1buffer store y = 1
2load y sees 0load x sees 0
3publish x = 1publish y = 1

Both loads read zero even though both stores were issued first, because neither store had been published when the loads ran.

Both threads can observe zero even though each thread placed its store before its load in source order.

The CPU does not need to literally execute the source instructions backward. The stores can remain buffered while the later loads obtain values. From the perspective of the other core, the loads became effective before the stores were visible.

Using C11 relaxed atomics makes this example a defined program whose operations intentionally impose very little cross-location ordering:

Plain C integers would introduce data races and undefined behavior, preventing this example from isolating the hardware ordering issue.

Loading simulation...

Atomicity vs. Ordering

Atomicity ensures that one operation on one object is indivisible. Ordering constrains how several operations may be observed relative to one another.

An atomic store to ready guarantees that a consumer does not read a torn flag. It does not automatically make preceding payload writes part of the same atomic event.

A memory-ordering constraint does the opposite kind of work. It can require the payload writes to become visible before publication, but it does not turn an ordinary shared increment into an atomic read-modify-write operation.

MechanismWhat it guarantees
Atomic operationThe indivisibility of one supported transition
Ordering constraintWhat can be observed across multiple operations

Correct concurrent code often needs both properties, but they answer different questions:

Cache Coherence vs. Memory Consistency

Modern multiprocessors normally maintain cache coherence. If several cores cache the same memory location, the coherence protocol coordinates writes so that they do not keep permanently conflicting versions.

Coherence is primarily a per-location property. All cores must agree on a sensible order for writes to x.

That does not establish one universal order across different locations:

A memory consistency model, often shortened to memory model, defines which combinations of observations are legal across locations and threads.

The hardware architecture has a memory model. The programming language has one as well. The compiler must translate the language guarantees into instructions that remain correct under the hardware guarantees.

Portable application code should therefore use the language's synchronization operations. Assuming behavior from one CPU architecture can fail after recompilation for another architecture, and using a special instruction does not repair source code that violates the language memory model.

Why Compilers Also Reorder Memory Operations

The processor is only one source of reordering. A compiler may:

  • Keep a value in a register instead of reloading it
  • Eliminate a load or store it considers redundant
  • Move independent operations across one another
  • Combine several operations into a different instruction sequence

For valid single-threaded code, these transformations preserve observable behavior under the language's as-if rule: the optimized program must behave as if it followed the abstract language rules.

Unsynchronized concurrent access to ordinary objects is not protected by those expectations in C. If a program has a data race, the compiler can make optimizations based on the fact that valid executions do not contain that race.

This is why inspecting generated assembly is not enough to prove thread safety. A particular compiler version may currently produce the expected order, but the source code still lacks a language-level guarantee.

Synchronization operations communicate constraints to both layers:

  1. Source-level atomic ordering constrains compiler transformations.
  2. Those become architecture-specific instructions and barriers, which constrain hardware observations.
  3. Together they produce the required cross-thread behavior.

Relaxed Atomic Operations

A relaxed atomic operation preserves the atomicity of that operation without establishing a general ordering relationship with other memory accesses.

For example:

Competing atomic increments cannot lose updates. All modifications of this atomic counter still have a well-defined order. The operation does not promise that unrelated data written before the increment has been published to a thread that later reads the counter.

Relaxed ordering is appropriate only when the value does not carry a message about other memory.

A statistics counter is a common example. If a thread only needs to count completed events, the increment may require atomicity but no relationship with the contents of those events.

A readiness flag is different. Its purpose is to announce that other data is ready. Treating it as a relaxed atomic flag omits the very relationship the flag is meant to communicate.

Data Publication with Release and Acquire

A release operation publishes earlier work. An acquire operation that observes that publication makes the earlier work available before the acquiring thread continues with dependent shared access.

The producer can initialize an ordinary payload and then perform a release store:

The consumer uses an acquire load:

When the acquire load reads the value published by the release store, the operations form a synchronization relationship:

The release-store and the acquire-load are the only synchronizing pair here. Everything the producer wrote before its release is visible to the consumer after its acquire.

The payload writes happen before the consumer's payload reads in the language memory model. The ordinary payload accesses are therefore valid under this one-time publication protocol.

The relationship is conditional: the acquire must observe the release or a value connected to it. A load that still sees false has not acquired the payload.

The producer must also stop modifying the payload while the consumer reads it. Release and acquire publish the completed state; they do not make future unsynchronized modifications safe.

Sequential Consistency

Sequentially consistent atomic operations provide a model that is easier to reason about. All such operations participate in one global order that is consistent with the program order of each thread.

Conceptually:

The basic C atomic operations use sequential consistency by default:

Sequential consistency does not make an entire function atomic, and it does not legitimize data races on nearby non-atomic objects. It provides stronger ordering for participating atomic operations.

Stronger ordering can require extra compiler or hardware constraints on some architectures. The performance difference depends on the operation and target CPU; it should be measured rather than assumed.

Use the weakest ordering that is clearly correct only when the synchronization argument is understood. A slightly stronger operation is often preferable to an incorrect optimization whose effects appear only under rare schedules.

Memory Barriers and Fences

A memory barrier, or memory fence, constrains memory operations across a point.

At a simplified level:

A barrier separates the operations issued before it from those issued after it.

The exact constraint depends on the barrier. Some order loads, some order stores, and some order both. Architectures expose different instructions because their default memory models differ.

There are also two layers of barrier:

  • A compiler barrier restricts how the compiler moves operations.
  • A hardware barrier restricts how the processor makes memory effects observable.

A compiler barrier alone cannot force another core to observe stores in the required order. A hardware barrier inserted into source code without compiler semantics can still be undermined by compiler movement. Correct language atomics let the compiler provide both parts as needed.

C exposes atomic_thread_fence for algorithms that require a standalone fence. Fences are easy to misuse because they do not identify the communicating object by themselves. Release and acquire operations on the publication variable usually express a simple handoff more directly.

A barrier is not a command to write every dirty cache line to RAM. Cache coherence and memory ordering are related but distinct mechanisms. A barrier enforces an ordering guarantee; the hardware may satisfy it through buffers, coherence messages, and architecture-specific instructions without flushing all caches.

Strong and Weak Hardware Memory Models

Processor architectures permit different default reorderings.

x86-64 has a relatively strong memory model. Ordinary cached stores are observed in store order, but a later load from another address can become effective before an earlier store is globally visible, as in the store-buffer example.

Architectures such as AArch64 and RISC-V permit more kinds of reordering unless instructions carry ordering semantics or explicit barriers are used.

“Strong” does not mean “concurrent code needs no synchronization.” Compiler transformations still exist, store buffering still matters, atomic read-modify-write operations are still required for compound updates, and the language memory model still governs source validity.

Operating-system kernels use architecture-aware barrier interfaces because the same kernel source must run correctly on machines with different ordering rules. Application code receives the same portability benefit from language atomic and synchronization APIs.

False Sharing: Independent Data, Shared Cache Line

Cache coherence operates at cache-line granularity, not variable granularity. A line is commonly 64 bytes on current general-purpose processors, although software must not assume that every machine uses that size.

Suppose two frequently updated counters are adjacent:

They may occupy one cache line:

The two counters are separate variables that no thread shares. The hardware does not track them separately, though, because ownership is granted for the whole line.

The threads do not access the same variable. Their updates are logically independent and can be completely race-free.

The hardware cannot transfer ownership of only the bytes containing first. When Core 0 obtains the line for writing, Core 1's copy of the whole line can be invalidated. Core 1 must then regain write ownership to update second, invalidating Core 0's copy.

The line bounces between the cores on every increment. Each transfer costs real time, and the program never asked for any sharing at all.

This repeated movement is often called cache-line bouncing.

False sharing occurs when threads modify different data that happens to share a cache line, causing unnecessary coherence traffic.

It is “false” because the program does not logically share the variables. The hardware sees sharing because its coherence unit is the containing cache line.

Loading simulation...

True Sharing and False Sharing

With true sharing, threads access the same data:

The coherence traffic reflects a real dependency. If every request must update one exact counter immediately, the conflicting writes are inherent in that design.

With false sharing, threads access distinct data:

The dependency is accidental and may be removed by changing the layout.

False sharing is usually a performance problem rather than a correctness problem. Both counters can end with the right values while execution takes much longer than expected.

This produces a characteristic symptom: adding threads increases CPU activity but provides little throughput improvement, even though code review finds no shared logical counter or obvious critical section.

Separating Frequently Written Data

One repair is to place independently written counters on separate cache lines:

Because array elements must preserve the alignment of their type, each padded_counter begins on a 64-byte boundary and its size includes enough trailing space for the next element to remain aligned on common C implementations that support this alignment.

Alignment alone is not always separation:

The structure begins at a line boundary, but second may still be placed immediately after first in the same line.

The hard-coded size is also platform-specific. Production code should obtain an appropriate cache-line or destructive-interference size from its supported platform, or isolate the layout decision behind a configuration boundary.

Other useful strategies are to:

  • Keep frequently written per-thread data genuinely thread-local and combine it less often.
  • Separate read-mostly fields from fields updated on hot paths.
  • Group fields by which thread writes them rather than only by conceptual meaning.

Padding has a cost. It increases memory footprint and can reduce useful cache density. It is most valuable for frequently written data accessed concurrently by different cores, not as a blanket rule for every structure.

A False-Sharing Benchmark

The following program compares two atomic counters stored next to each other with two counters aligned into separate 64-byte slots.

Relaxed atomic increments make every iteration a defined, indivisible update without publishing other data. They also prevent the compiler from replacing the entire loop with one final addition.

Compile and run it:

The exact times are machine- and run-dependent. If the threads execute on different cores and the assumed line size matches the machine, the separated layout will often be substantially faster. If both threads share one core, the difference may be small because the line is not bouncing between private caches.

Run the program several times and compare the pattern, not one measurement. CPU migration, frequency scaling, other system activity, and virtualized hardware can all add noise.

On a Linux system with supported performance-monitoring events, perf c2c can help attribute cache-to-cache traffic to contended lines:

Availability and required permissions vary. Timing shows that the layouts behave differently; hardware counters provide stronger evidence that cache-line movement is the cause.

Diagnosing False Sharing in Real Services

False sharing is worth investigating when a workload has all of these characteristics:

  • Several threads write frequently.
  • The writes target different fields or per-worker objects.
  • Throughput stops scaling as cores are added.
  • CPU usage remains high even though useful work does not increase proportionally.

Start with ownership and layout. Determine the address of each hot field, its offset within the containing structure, and which threads modify it. Two fields with different names or array indices can still occupy the same line.

Then measure. A benchmark should preserve the real write frequency and thread placement closely enough to reproduce the effect. Hardware performance counters or cache-to-cache analysis can help distinguish false sharing from ordinary cache misses, scheduling overhead, or true contention.

After changing the layout, verify both throughput and memory use. A faster microbenchmark is useful, but the production trade-off also includes a larger working set and the behavior of the complete workload.

Summary

Memory ordering defines which effects one execution path must observe after it observes an event from another. Atomicity protects one operation; ordering connects multiple operations. Cache coherence keeps writes to a location consistent, but it does not by itself publish related data across locations.

Relaxed atomics provide indivisible operations with minimal cross-location ordering. Release and acquire operations establish a publication relationship, while sequentially consistent atomics provide a stronger global ordering model. Language atomics allow the compiler to enforce these guarantees on architectures with different hardware memory models.

False sharing occurs when independent, frequently written variables occupy the same cache line. Their values remain correct, but the line bounces between cores and limits scalability. Separating hot writable data, measuring cache-to-cache traffic, and balancing padding against memory footprint can remove this accidental hardware contention.

Quiz

Memory Ordering, Barriers, and False Sharing Quiz

5 quizzes