Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 61

Memory Barriers: Compiler, Volatile, and Hardware

Recommended resources

Hello! Welcome to your next lesson in the Embedded Interview Preparation Bootcamp.

In our previous session on RCU, we saw how primitives like rcu_assign_pointer work their magic, allowing for scalable, lock-free reads. We briefly mentioned that these primitives rely on underlying "memory barriers" to ensure that updates are published correctly across all CPU cores. Today, we peel back that layer of abstraction to understand what's really happening.

This topic is at the heart of low-level systems programming and is a frequent subject in senior-level interviews at semiconductor companies. A clear understanding of memory ordering separates an application developer from a true systems engineer.

Your learning outcome for this lesson is to: Distinguish between compiler barriers, volatile keyword semantics, and explicit hardware memory barriers for enforcing memory ordering.

1. The Two Beasts of Reordering: Compiler and CPU

When you write C code, you intuitively expect the operations to happen in the order you wrote them. However, for performance, both the compiler and the CPU are allowed to reorder your instructions, as long as they don't change the outcome for a simple, single-threaded program.

  1. Compiler Reordering: The compiler reorganizes your code during compilation to generate more efficient assembly.
  2. CPU Reordering: A modern processor executes instructions out-of-order to keep its execution units busy, hiding memory latency. This is especially prevalent on weakly-ordered architectures like ARM.

In a concurrent system (multi-core, or with interrupts), this reordering can break your code in subtle and hard-to-debug ways. To regain control, we need to issue specific commands—barriers—to both the compiler and the CPU.


2. Taming the Compiler: volatile and Compiler Barriers

Let's start with the compiler. Its goal is to produce fast code. If it sees an opportunity to rearrange or even eliminate memory accesses without changing the single-threaded program logic, it will take it.

The volatile Keyword: A Limited Tool

A common tool to control the compiler is the volatile keyword. Its C standard definition is narrow but precise: every access to a volatile-qualified object in the abstract C machine must correspond to an actual load or store on the real machine.

This is essential for two main scenarios:

  • Memory-Mapped I/O (MMIO): Accessing a hardware register whose value can change at any time.
  • Variables shared with an ISR: Ensuring the main code sees changes made by an interrupt handler.

Nine ways to break your systems code using volatile

To start, let's read a concise explanation of what volatile means and see classic examples where omitting it leads to incorrect code. This article by John Regehr is a widely cited authority on the topic.

Read the sections 'What does volatile mean?' and '1. Not enough volatile'. Pay close attention to how the compiler optimizes away reads from the timer register and the done flag, and how volatile forces the compiler to emit the necessary load instructions.

The Critical Limitation of volatile

Here is the key interview question: Is volatile a memory barrier? No.

volatile only guarantees that accesses to that specific volatile variable are not optimized away. It provides no ordering guarantees with respect to other non-volatile memory accesses. The compiler is free to move a non-volatile store or load from before a volatile access to after it, or vice versa.

This is one of the most misunderstood concepts in C. Let's look at a canonical example of where this fails.

Nine ways to break your systems code using volatile

The same article has an excellent section that demonstrates how major compilers can reorder a non-volatile access around a volatile one, breaking the programmer's intent.

Now, read section '5. Expecting volatile to enforce ordering with non-volatile accesses'. The ready flag example is crucial. See how the write to ready is moved before the write to message, and how a 'compiler barrier' is introduced to fix it.

Compiler Barriers: The Real Solution for Ordering

As you just read, to prevent the compiler from reordering memory operations around a specific point, we need an explicit compiler barrier. In GCC and compatible compilers, this is:

asm volatile ("" ::: "memory");

This inline assembly statement tells the compiler:

  1. It contains no actual instructions ("").
  2. It is volatile, so it must not be removed.
  3. It "clobbers" memory ("memory"). This is the key part. It tells the compiler to assume that this operation could read or write any location in memory.

This forces the compiler to:

  • Commit any modified register values to memory before the barrier.
  • Not move any memory accesses from before the barrier to after it.
  • Not move any memory accesses from after the barrier to before it.
Memory Ordering with and Without Barriers
This diagram illustrates the effect of a barrier. Without it, operations can be reordered by the compiler or CPU. A barrier acts as a fence, ensuring all operations before it are completed before any operations after it begin.

For a more comprehensive look at compiler reordering and barriers, the following article provides another excellent perspective, including the evolution of volatile usage in the Linux kernel to macros like READ_ONCE.

Memory Model and Synchronization Primitive - Part 1

This article from Alibaba Cloud provides another clear explanation of compiler reordering and the role of compiler barriers, contrasting barrier() with volatile and ACCESS_ONCE().

Read the 'Compiler Barrier' section. Focus on the explanation of how barrier() works using the "memory" clobber and how it differs from using volatile on a variable.


3. Taming the CPU: Hardware Memory Barriers

Now, assume you've used compiler barriers correctly. Your compiled assembly instructions are in the exact order you need. You're still not safe! A modern CPU, especially a weakly-ordered one like ARM, can still execute these instructions out of order.

The most intuitive reason for this is the store buffer. To avoid stalling on slow memory writes, a CPU core will often write data into a small, fast buffer (the store buffer) and then continue executing subsequent instructions. This means a store might become visible to other cores much later than a subsequent load from the same core.

Advanced Topics: Hardware Memory Barriers

This video gives a very accessible explanation of CPU reordering by visualizing the role of the store buffer on an x86 processor.

Watch the first three minutes (00:00 - 02:58). Focus on understanding how the store buffer allows a load to effectively bypass an earlier store, causing memory operations to become globally visible out of program order.

On strongly-ordered architectures like x86, this store-load reordering is one of the few reorderings allowed. On weakly-ordered architectures like ARM, the rules are much more relaxed—almost anything can be reordered unless there is a direct data dependency.

This is where hardware memory barriers come in. These are actual CPU instructions that force the processor to enforce an ordering on its memory operations.

The following video is a fantastic deep dive into ARM's memory model, presented by an expert from Amazon. This is exactly the level of detail expected in a senior interview at a company like ARM or Qualcomm.

Arm's Weakly-Ordered Memory Model and Barrier Requirements - Ash Wilding, Amazon

Let's explore the weakly-ordered memory model of Arm processors and the barriers required to manage it.

Watch from 02:50 to 13:35. This covers three critical points: What weak ordering means (02:50 - 07:58): Understand that non-dependent loads/stores can be reordered. A classic example (07:58 - 11:29): See how a simple polled mailbox between two cores can fail without barriers. The CPU barrier solution (11:29 - 13:35): Learn the difference between CPU barriers and compiler barriers, and what a CPU barrier instruction actually does.

Types of Hardware Barriers

As you saw in the video, hardware barriers are not one-size-fits-all. To maximize performance, architectures provide different types of barriers for different ordering needs.

On ARM, the main barrier instructions are:

  • DMB (Data Memory Barrier): Ensures that all memory accesses before the DMB are observed before any memory accesses after the DMB. It doesn't stop execution, but it enforces ordering of memory operations.
  • DSB (Data Synchronization Barrier): Stricter than DMB. It blocks execution of any instructions (not just memory accesses) until all memory accesses before the DSB are complete. You need this when you're changing system state, like page tables, and need to ensure the change is complete before proceeding.
  • ISB (Instruction Synchronization Barrier): The strictest barrier. It flushes the pipeline and prefetch buffers, causing instructions after the ISB to be re-fetched from memory. This is necessary after modifying self-modifying code or changing MMU settings that affect instruction fetches.

These barriers can be further qualified by the types of accesses they order (e.g., ST for store) and the group of observers they apply to (e.g., ISH for Inner Shareable, i.e., other cores in the same cluster).

Arm's Weakly-Ordered Memory Model and Barrier Requirements - Ash Wilding, Amazon

Let's continue with the same video to understand these different barrier types. Knowing when to use a DMB versus a DSB is a classic advanced topic.

Watch from 13:35 to 19:00 to understand DMB qualifiers (ISH, OSH, LD, ST) and the concept of minimally intrusive barriers. Then, jump to 28:20 - 31:43 to see how DSB and ISB are used in a more complex scenario of modifying program memory.

Memory Barriers in Producer-Consumer Pattern
This diagram shows a practical application in a producer-consumer model. The producer uses a store-store barrier (`membar_ss`) to ensure the new data is visible before the tail pointer is updated. The consumer uses a load-load barrier (`membar_LL`) to ensure it reads the head pointer before reading the data, preventing it from accessing stale data.

4. Summary: The Three Layers of Ordering

Let's synthesize everything into a clear comparison. This is your key takeaway for an interview.

Primitive What It Does Scope Analogy
volatile variable Prevents the compiler from optimizing away loads/stores to that specific variable. Compiler A "Do Not Remove" sign on a single line of code.
Compiler Barrier (asm volatile("" ::: "memory");) Prevents the compiler from reordering any memory access across the barrier. Compiler A fence in your source code that the compiler cannot move instructions across.
Hardware Barrier (dmb, mfence, etc.) Prevents the CPU from executing/making visible memory accesses out of order across the barrier. CPU Hardware A command to the CPU's traffic controller to drain all buffered operations before letting new ones proceed.

In short:

  • Use volatile for MMIO or variables shared with an ISR to ensure visibility.
  • Use a compiler barrier to enforce ordering between accesses to different variables at compile time.
  • Use a hardware barrier to enforce ordering at runtime on the CPU itself.

In the Linux kernel, a full memory barrier like smp_mb() includes both a compiler barrier and a hardware barrier to provide a complete ordering guarantee against both threats.


Conclusion

You have now delved into the core of memory ordering in concurrent systems. You've distinguished between the reordering performed by the compiler and the CPU, and you've learned about the specific tools used to control each one: volatile, compiler barriers, and hardware barriers. Understanding this hierarchy is non-negotiable for writing correct, high-performance, low-level code.

Key Takeaways:

  • volatile is not a memory barrier. It only prevents compiler optimizations on accesses to a single variable.
  • Compiler barriers (asm volatile...) control the compiler, preventing it from reordering memory operations in the generated assembly code.
  • Hardware barriers (dmb, mfence) control the CPU, preventing it from executing memory operations out-of-order at runtime.
  • Correct lock-free synchronization requires a combination of these primitives to guard against both compiler and CPU reordering.

Preview of the Next Lesson:

We've now covered the most challenging aspects of kernel concurrency. Next, we will shift our focus within the kernel to another fundamental topic: memory management. We will start by exploring the two main functions for allocating kernel memory, kmalloc and vmalloc, and learn why you would choose one over the other. This is another classic interview topic that tests your understanding of virtual vs. physical memory within the kernel.