Understanding Memory Consistency in RISC-V: Caches, Barriers, and Atomics
The idea for this article emerged after reading numerous sources on memory barriers and memory models, as well as watching several…
Understanding Memory Consistency in RISC-V: Caches, Barriers, and Atomics
The idea for this article emerged after reading numerous sources on memory barriers and memory models, as well as watching several explanatory talks and lectures on the topic. Many of those materials provide excellent, in-depth explanations. This article does not aim to be exhaustive — rather, it serves as an overview of how memory models relate to the RISC-V architecture, focusing on their interaction and practical implications. It’s not intended for beginners; readers are encouraged to go through some introductory material first to better understand the underlying concepts.
It’s unlikely that anyone really needs a detailed understanding of a processor’s internal architecture at the RTL (register-transfer level), or a deep grasp of memory models and consistency principles. Typically, all that knowledge comes from vendor documentation and various optimization books. I’d assume the authors of such materials might know more than what’s publicly available — perhaps because they’ve worked at companies like Intel. Networking, conferences, and communities also play a role here. But could there be any better source than the CPU’s own source code? Hardly. You won’t find the source code for Intel, AMD, or any other commercial processors publicly available. ARM, for instance, charges money for access — selling rights to the architecture and pre-designed components. However, for RISC-V, such a source does exist: the open-source code of a full-fledged superscalar out-of-order processor developed at Berkeley.
Not long ago, a new language called Chisel was developed as a replacement for Verilog and similar HDLs. It’s based on Scala. And since it’s Scala, we can use an IDE with proper navigation and code insights. But you can’t just dive into the source right away — you first need a solid foundation to understand what RTL-level design even means. It helps to study Verilog and the basic concepts — latches, registers, multiplexers, and so on. That’s exactly what I decided to do, starting with the book “Computer Architecture: A Quantitative Approach” by Patterson and Hennessy. It’s an absolutely brilliant and very accessible read. After that, I went through several books on FPGA and Verilog, and even bought a learning FPGA board — though I still haven’t gotten around to using it. That process helped me form a clearer picture of how CPUs function and how microcontrollers are designed.
Back to Chisel. There are several open-source CPU cores written in this language implementing the RISC-V architecture.
Sodor
This is an educational project implementing the most basic version of the RISC-V instruction set. The source code is indeed simple, allowing you to quickly grasp the general principles of CPU architecture design. It includes both a non-pipelined implementation and one with a simple multi-stage pipeline.
Next, there’s the so-called Rocket:
- *https://github.com/chipsalliance/rocket-chip*
- *https://chipyard.readthedocs.io/en/stable/Generators/Rocket.html*
This is an in-order 5-stage pipelined processor with an L1 cache. There’s also an additional L2 cache module of the directory-based type:
And the cherry on top — a fully-fledged out-of-order implementation from Berkeley, featuring memory barriers, atomic operations, and cache coherence mechanisms:
The Berkeley Out-of-Order RISC-V Processor (BOOM)
- *https://github.com/riscv-boom/riscv-boom*
- *https://chipyard.readthedocs.io/en/latest/Generators/BOOM.html*
It includes several versions of the core with different pipeline depths (7-stage and 12-stage).
For those who want to explore the source code, links are provided at the end of the article. The file naming conventions and instruction table structure are more or less consistent across all projects (Sodor, Rocket, and BOOM). Both BOOM and Rocket include L1 caches (separate for instructions and data) and support multi-core configurations. They use the TileLink protocol for communication with the outside world. TileLink handles requests to main memory, higher-level caches, and MMIO, while also implementing cache coherence. It serves as an alternative to MESI and similar protocols.
Below is the CPU & TileLink bus diagrams from the specification:


The basics of memory barriers.
Alright, let’s start from the very basics. Imagine we have a single-core processor. It has an L1 cache, higher-level caches, and main memory. From now on, we’ll call everything above L1 external memory. When the CPU reads data, it first loads an entire cache line (typically 64–128 bytes), and then extracts the required portion from it. When it needs to write data, it also must first load the full cache line, modify the relevant bytes, and mark the line as dirty, meaning it will need to be synchronized with main memory later. Other than that, there aren’t really alternative mechanisms for memory access. If the data isn’t in the cache, the CPU must wait a relatively long time to fetch it. That’s one of the reasons we have complex reordering and pipelining — to allow speculative reads and keep other instructions running while waiting for data.
Now, let’s imagine we have two cores, each with its own data cache. In this case, we need to know whether a given cache line exists only in one core’s cache, or if it’s shared between multiple cores. We also need to track whether the data has been modified locally by one of the cores. For example, core 1 wants to read a memory region. It loads that cache line and starts working with it. When core 2 tries to load the same line, it discovers that this cache line already exists in another core’s cache. There are multiple ways to handle this. If we have a higher-level cache, it can maintain metadata describing which cores share which cache lines. This is how the L2 cache module is implemented in the current Berkeley RISC-V architecture: *https://github.com/chipsalliance/rocket-chip-inclusive-cache*
Alternatively, we can manage this via bus messages, checking whether another core holds the line — and even retrieving data directly over the bus without going all the way to main memory. The TileLink protocol allows this. If we’re only reading the data, we don’t really care whether the line is shared or not — the data is consistent across cores, and no issues arise. The real problem begins when one of the cores modifies the data. As soon as that happens, the modifying core must somehow notify other cores that their copies are no longer valid. In the RISC-V TileLink protocol, this is handled via ownership levels. A node can have read-only (r/o) or read-write (r/w) access to a cache line. When loading a line, a core requests it with the desired ownership level. Only one node at a time can hold r/w ownership. If a core currently has r/o access but needs to write, it must first request an upgrade to r/w. The core sends a message on the bus requesting this permission change. If another core currently has r/w ownership, it must relinquish it and write back dirty data to external memory if the line was modified. After a core modifies data, it sends a Probe message on the bus to invalidate that line in all other caches where it exists with r/o ownership. This is known as the Probe Queue in RISC-V TileLink terminology. Other cores receive the probe and invalidate their copies.
However, invalidation doesn’t mean they immediately reload fresh data — nor does it mean the modifying core immediately writes the dirty line back to memory. The dirty line remains in the local cache until it’s evicted due to cache pressure or requested by another core. When another core later needs it, the system sends a message requesting the line, forcing a write-back. All these message transactions take significant time. The bus has arbiters that queue requests, and often a core must wait for responses from other nodes — this can take dozens or even hundreds of cycles, depending on bus speed and contention. The probe request also goes into the probe queue of the target core, and responses are typically sent before the probe is fully processed. This particular behavior gets a lot of attention in discussions of memory models, because while a probe is sitting in the invalidation queue but not yet processed, a core might continue speculative execution using cached data that is already inconsistent. In RISC-V BOOM, it works a bit differently. When a probe arrives and is placed in the probe queue, it isn’t handled immediately — it takes multiple cycles. However, even without explicit memory barriers, the BOOM implementation checks whether the address being read is present in the probe queue. Memory barriers, of course, prevent speculative reads and ensure that any load sees pending invalidations. In other words, if a probe arrives, a subsequent load instruction will observe it.
One of the most important questions here is: how quickly do data modifications in one core become visible to another core or to main memory? If a single core owns and modifies data exclusively, those changes simply remain in its cache until a write-back occurs — which might happen at any time. But as soon as another core needs the same data, it’s flushed to memory or transferred over the internal bus or shared cache. This isn’t instantaneous. Data may also be written back when cache lines are evicted due to space constraints.
We can analyze this using the example of false sharing.
Recall that false sharing occurs when two (or more) variables are located close together in memory and end up sharing the same cache line. The threads using them might have no idea, since logically the variables are unrelated. For example, the first variable is used exclusively by thread 1, and the second by thread 2. But because both threads continuously read and write their own variables, they generate excessive bus traffic — since they both keep invalidating and writing back the same shared cache line.
In short: the variables lie next to each other on the same cache line.
long a;
long b;
thread 1:
while (true) a++;
thread 2:
while (true) b++;
- Thread 1 reads variable a.
- A bus request is sent to load the cache line with r/w (read-write) ownership.
- The cache line is loaded.
- Thread 1 modifies the line, marking it as dirty.
- Thread 2 reads variable b with r/w ownership.
- A message arrives on the bus to revoke r/w ownership from core 1.
- Core 1 writes back the data to external memory and invalidates the cache line.
- The cache line is loaded into core 2’s cache.
- Thread 2 modifies the line, marking it as dirty.
- Thread 1 reads variable a with r/w ownership.
- A message arrives on the bus to revoke r/w ownership from core 2.
- Core 2 writes back the data to external memory and invalidates the cache line.
- The cache line is loaded into core 1’s cache.
- Thread 1 modifies the line, marking it as dirty.
- And the cycle repeats…
In the case where both cores hold the same cache line with r/o (read-only) permissions, and one of the threads wants to modify it, a bus request is issued to upgrade ownership to r/w. This, in turn, immediately causes the other core to invalidate its own copy of the line. The act of writing to the cache line is what triggers this ownership upgrade and invalidation request.
Sending a message on the bus happens in two stages:
- The message is first sent to the bus arbiter.
- The arbiter then contacts all relevant nodes, forwards the original request, waits for their responses, and finally returns the combined result.
The bus is relatively slow — message transfers can take dozens of cycles. Writing a dirty cache line back to memory and then reloading it into another core’s cache is also a slow operation. As a result, each iteration of variable reads by threads 1 and 2 can slow down the algorithm by tens or even hundreds of times. By analogy, it’s easy to imagine similar slowdowns in other algorithms. For example, Dekker’s algorithm, in light of this behavior, looks extremely unfriendly to both the bus and CPU caches. In that case, you don’t even have false sharing — the threads are explicitly sharing the same variables. And that’s without considering memory barriers. If barriers are used, things can become even slower due to forced queue flushes, prefetch invalidations, and other synchronization effects.
Memory Barriers in RISC-V
Now we can move on to a more detailed examination of the CPU’s internal mechanisms and memory barriers. In RISC-V, the memory barrier instruction is called FENCE (as in many other architectures). From the specification:
“The FENCE instruction is used to order device I/O and memory accesses as viewed by other RISC-V harts and external devices or co-processors. Any combination of device input (I), device output (O), memory reads (R), and memory writes (W) may be ordered with respect to any combination of the same. Informally, no other RISC-V hart or external device can observe any operation in the successor set following a FENCE before any operation in the predecessor set preceding the FENCE”.
Moreover, the FENCE instruction supports parameters that define the type of memory barrier, such as LoadLoad, StoreStore, LoadStore, StoreLoad, and TSO. In the current implementation of RISC-V BOOM, these variants are not actively used. In general, the FENCE instruction, atomics, and the Load-Reserved/Store-Conditional (LR/SC) pair align quite well with the C++ memory model — and now, practically, with Java’s VarHandle API as well. As for atomics, the RISC-V specification provides an excellent description of the RVWMO (RISC-V Weak Memory Ordering) model. The FENCE, atomic, and LR/SC instructions all include additional parameters that define the required level of serialization.
From the specification:
Load and store operations may also carry one or more ordering annotations from the following set: “acquire-RCpc”, “acquire-RCsc”, “release-RCpc”, and “release-RCsc”. An AMO or LR instruction with aq set has an “acquire-RCsc” annotation. An AMO or SC instruction with rl set has a “release-RCsc” annotation. An AMO, LR, or SC instruction with both aq and rl set has both “acquire-RCsc” and “release-RCsc” annotations.
Here’s a little teaser — a diagram of the RISC-V BOOM core.

Exactly — the Execution Unit (EXU) is one of the most intricate and fascinating parts of BOOM’s microarchitecture. It’s where all the out-of-order magic happens: instruction dispatch, scheduling, dependency tracking, speculation, and reordering. Here’s what’s noted in the code comments (summarizing the structure you’re referring to):
Load/Store Unit is made up of the Load-Address Queue, the Store-Address Queue, and the Store-Data queue (LAQ, SAQ, and SDQ). Stores are sent to memory at (well, after) commit, loads are executed optimistically ASAP. If a LoadAddr and StoreAddr match, the Load can receive its data by forwarding data out of the Store-Data Queue. Currently, loads are sent to memory immediately, and in parallel do an associative search of the SAQ, on entering the LSU. If a hit on the SAQ.
Stores are executed as soon as they are committed by the out-of-order Reorder Buffer (ROB). If a FENCE instruction is present, it enforces program order for all operations — both in the Execution Unit’s ROB and in the Load/Store Queues (LDQ/STQ). The same applies to prefetching: FENCE prevents later read requests from being issued too early and invalidates speculative cache-line reads. We’ll leave the TLB out of scope here — that’s a separate and rather complex topic. Inside the Load-Store Unit (LSU) there are several functional blocks, connected by arbiters that queue and schedule memory requests between them. Let’s now draw a more detailed diagram of the LSU together with the L1 data cache (L1$D) (Apologies for my lack of skill in making clear diagrams) .

Got it. Let’s connect this architecture to how memory barriers (FENCE) actually behave inside the CPU pipeline.
We’ve established the main LSU components:
- Exec Unit — the core itself, managing instruction issue and retirement.
- LDQ / STQ — the load and store queues, where memory ops live until safe to issue or commit.
- DATA / META — the L1 data cache’s data and tag arrays, respectively.
- TileLink — the coherence bus interface to external memory and other cores.
- Probe — handles invalidation and ownership-change requests.
- WriteBack Unit (WB) — flushes dirty cache lines to memory.
- MSHRs — handle outstanding misses and cache line fills or evictions.
Now, consider how FENCE fits in.
When the processor executes a FENCE instruction:
- The Reorder Buffer (ROB) stops committing younger instructions until all older memory operations have completed (i.e., they’ve retired from the LDQ/STQ and are globally visible).
- The Load/Store Unit drains pending operations: STQ must commit all stores to the cache (and possibly to memory). LDQ must ensure no speculative loads remain unresolved or awaiting probes.
- The LSU’s arbiters synchronize all internal requests — no new memory ops are issued until the barrier clears.
- The prefetcher, if active, is also stalled or its speculative reads invalidated, ensuring no cache lines are fetched prematurely.
- The cache may flush or invalidate speculative entries depending on architecture-specific FENCE semantics.
The result is that FENCE enforces program order:
- The ROB can’t commit instructions past it.
- The LDQ/STQ can’t reorder or speculate across it.
- The cache and MSHRs must complete all outstanding requests.
Under normal conditions, LDQ and STQ interact freely with the L1$D and MSHRs — speculative loads may issue, and stores are buffered until commit. But under FENCE, those interactions are serialized:
- Loads must retire before subsequent stores.
- No speculative or out-of-order memory accesses occur.
The ROB, LSU, and TileLink all synchronize before continuing.
Now, let’s move to your “code example” part — the kind of snippet where FENCE semantics actually matter (e.g., reordering between stores and loads). Could you show me the code you want to use as an example? I’ll explain exactly how it behaves in the LSU and on the TileLink bus.
f = 0, x = 0
thread 1:
x = 42;
// Memory fence required here
f = 1;
thread 2:
if (f == 1);
// Memory fence required here
print x; // should be 42
When we observe f == 1 in one thread, we naturally assume that x == 42 as well — since the writes to these variables were performed in that order. The crucial point of using memory barriers is to ensure that the external world sees the data in the same order as they were written in the program order — both for writes that go directly to memory (e.g. MMIO) and for cache line updates. This also includes various bus messages: data reads, ownership upgrades, invalidations in other cores, and so on. In reality, without memory barriers, this guarantee doesn’t hold. The compiler or the processor may reorder instructions freely — which is allowed, because logically they are unrelated from the hardware’s point of view (even if we, as programmers, know they are connected).
For now, let’s assume the compiler doesn’t reorder or optimize anything. In CPU terminology, dependencies are called hazards — data, address, or register hazards. The Execution Unit tracks these dependencies to prevent breaking them (e.g., executing something before the previous operation completes). However, it cannot infer logical relationships between different data values. As a result:
- Memory writes may enter the store queue in one order,
- Be placed on the coherence bus in another order,
- And invalidations may arrive in yet another order on other cores.
Thus, we might observe x == 0 even after reading f == 1.
In BOOM’s implementation, you can see the Reorder Buffer (ROB) and Rename Stages — this is exactly what we mean by instruction reordering. When the CPU decodes multiple instructions concurrently, it performs register renaming if several instructions use the same architectural register but are independent. This is handled via shadow (physical) registers, and instructions are issued out of order and placed into the ROB. Once executed, results are written back to the architectural registers in program order during commit. BOOM, for example, has over a hundred physical registers for this purpose. The key point: Instructions commit (and thus memory writes occur) out-of-order depending on readiness. The FENCE instruction, however, prevents earlier memory operations from entering the ROB until the barrier completes. As a result:
- They are not reordered
- They reach the store queue (STQ) in program order
- And from the STQ, data is flushed to the cache in the same order
All requested addresses are also placed in the Load Queue (LDQ) — which, under normal conditions, may execute out of order due to speculative prefetching or load speculation. FENCE forces both LDQ and STQ to operate in program order. Additionally, the LSU supports store-to-load forwarding, meaning that a load can read data directly from the store queue if it refers to the same address and the store hasn’t yet reached the cache. If the data isn’t in the cache, the address is sent to an MSHR (Miss Status Holding Register) — which interacts with main memory via TileLink, fetching the corresponding cache line. This applies to both loads and stores — the cache line must be fetched before modification. The only exception is MMIO or uncacheable memory regions, where data is directly read or written without caching.
In RISC-V BOOM, memory barriers are not no-ops — they have real, nontrivial implementations due to the presence of aggressive optimizations. The design was indeed inspired by the DEC Alpha architecture.
A few notes on Java volatile
Now, a small but important note about volatile in the Java language. It’s not as intuitive as explicit memory barriers in systems languages like C++ or Rust, and many developers don’t fully understand how the compiler actually translates volatile and inserts the corresponding memory barriers.
volatile int f;
int x;
.....
x = 42;
f = 1;
In fact, you’re not creating a barrier for f itself, but for everything that happens before f — in this case, for x. And for all reads after f in another threads. Yes, the volatile keyword in Java also guarantees atomic read/write operations for variables like long and double, even on architectures where such operations wouldn’t normally be atomic. But that’s not what we’re focusing on here. When we talk about memory barriers, we’re always referring to dependencies between variables — the ordering relationship between them.
In our earlier example, we declared f as volatile, because x depends on it. The logic is: once f becomes visible to another thread, we assume that all prior writes (like the write to x) have also become visible.
In the other thread, the pattern is reversed:
- It writes to some variables before writing to f.
- It reads those variables after reading f.
Thus, f acts as a synchronization point — not because of its own value, but because of the memory ordering guarantees it enforces around itself.
In terms of how this maps to hardware:
- When a thread writes to a volatile variable (f = 1), the JVM inserts a StoreStore and StoreLoad barrier — ensuring that all prior writes (like x = 42) are globally visible before the volatile write.
- When another thread reads that volatile variable, it enforces LoadLoad and LoadStore barriers — ensuring that no subsequent reads or writes are performed before the volatile read completes.
That’s why, when one thread sees f == 1, it can safely assume x == 42. The visibility of f implies the visibility of everything before it in program order.
if (f == 1) print x;
It doesn’t matter whether we write or read a single additional variable or many of them, as in the case where a volatile field refers to an object with internal members or to an array rather than a simple scalar type. What matters is that when we obtain a pointer to such an object, we must be sure that the object itself is read in a consistent state — all its internal fields must reflect a valid snapshot.
Atomics in RISC-V.
It’s quite interesting that atomic operations are implemented both in the Load-Store Unit (LSU) and in the TileLink protocol — separate from the main ALU. The first implementation is used for cacheable memory, while the second handles non-cacheable memory. For example, in the case of an atomic increment on cacheable memory, the processor first acquires the cache line with read-write permissions, then performs the operation using the ALU inside the Load-Store Unit, rather than the core’s main ALU. After that, the cache line simply remains in the cache until another CPU requests it for reading or modification. Interestingly, I haven’t found anything in the code indicating that the line is forcibly written back to main memory after an increment. For non-cacheable memory, the CPU sends a message over the TileLink bus containing all the necessary information — which operation to perform, with what operands, and at what address. The target node receives this message and executes the operation. TileLink actually implements the logic for atomics directly at the protocol level within the bus nodes. Here’s an interesting question that arises: how can the logic break down without atomics, given that when we read a cache line with write permissions, we get exclusive ownership? We modify it, and any other core requesting that line must first force our core to write back the updated data before reading it. At first glance, it seems like caches would always contain the most recent value between cores. That’s true — but only when atomics are implemented as a single instruction. In RISC-V, there are no dedicated “increment value at address” instructions. Instead, this can be expressed using three separate operations — load, modify, and store — because RISC-V follows a load-store architecture, where we first load data into a register, perform computations, and only then write it back to memory.
Non-atomic increment (load–modify–store)
li t0, 48 // load immediate: t0 = 48
lw t1, 0(t6) // load word from memory[t6] into t1
// context switch, interrupt, or cache invalidation may happen here
addi t1, t0, 1 # t1 = t0 + 1
// by this time, memory[t6] may have been changed by another core
sw t0, 0(t6) // store t0 back to memory[t6] (overwriting newer value)
The logic of atomic operations for cacheable memory is implemented in the Load-Store Unit (LSU), as mentioned earlier. First, the data is fetched with read-write ownership, then the operation is performed directly within the LSU, updating the value inside the cache line. If a request to revoke ownership arrives, it will be handled either before the atomic operation executes or after the value has been updated. In that case, the modified value will be written back to memory and transferred to another core’s cache, where the same atomic operation will then be performed. Interestingly, in the RISC-V BOOM implementation, atomics do not have explicit memory-barrier attributes, though in some cases they do require additional optimization constraints. That’s why, in the BOOM CPU source code, atomic instructions often appear in logic closely tied to the handling of FENCE operations. And here’s perhaps the most fascinating part: RISC-V intentionally chose the Load-Reserved / Store-Conditional (LR/SC) approach, because it’s not susceptible to the ABA problem. The RISC-V specification contains a long and detailed explanation of why that’s the case — here’s the key idea from it:
*Both compare-and-swap (CAS) and LR/SC can be used to build lock-free data structures. After extensive discussion, we opted for LR/SC for several reasons:*
*CAS suffers from the ABA problem, which LR/SC avoids because it monitors all accesses to the address rather than only checking for changes in the data value;**CAS would also require a new integer instruction format to support three source operands (address, compare value, swap value) as well as a different memory system message format, which would complicate microarchitectures;**Furthermore, to avoid the ABA problem, other systems provide a double-wide CAS (DW-CAS) to allow a counter to be tested and incremented along with a data word. This requires reading five registers and writing two in one instruction, and also a new larger memory system message type, further complicating implementations;**LR/SC provides a more efficient implementation of many primitives as it only requires one load as opposed to two with CAS (one load before the CAS instruction to obtain a value for speculative computation, then a second load as part of the CAS instruction to check if value is unchanged before updating). The main disadvantage of LR/SC over CAS is livelock, which we avoid, under certain circumstances, with an architected guarantee of eventual forward progress as described below. Another concern is whether the influence of the current x86 architecture, with its DW-CAS, will complicate porting of synchronization libraries and other software that assumes DW-CAS is the basic machine primitive. A possible mitigating factor is the recent addition of transactional memory instructions to x86, which might cause a move away from DW-CAS. More generally, a multi-word atomic primitive is desirable, but there is still considerable debate about what form this should take, and guaranteeing forward progress adds complexity to a system. Our current thoughts are to include a small limited-capacity transactional memory buffer along the lines of the original transactional memory proposals as an optional standard extension “T”*
Load-Reserved / Store-Conditional (LR/SC) works differently from Compare-And-Swap (CAS). First, we reserve a memory address using the Load-Reserved (LR) instruction. We are then given a limited number of cycles to complete our logic — 16 cycles in the case of RISC-V. After that, we attempt to update the value in memory using Store-Conditional (SC), comparing it to the previous one, just like in a classic CAS operation. However, unlike CAS, the LR/SC pair will fail even if the value was rewritten to the same value — as long as another write occurred to that address in the meantime.
CAS implemented using LR/SC:
cas:
lr.w t0, (a0) // load from reserved ptr
// program business logic
sc.w t0, a2, (a0) // try to update
bnez t0, cas // repeat if fail
What’s most interesting is that if the data is not present in the cache, we most likely won’t manage to fetch it within the 16-cycle reservation window. As a result, the SC will fail, and we’ll keep spinning in the CAS loop until the cache line is finally pulled into the cache — with read/write ownership, of course. Once the update completes successfully, the modified cache line remains in the cache until it’s evicted or until a coherence request arrives over the bus for that line. The SC will also fail if a context switch occurs in the process, since the reservation is lost when the context is changed. Interestingly, in the current implementation of RISC-V BOOM, the SC instruction does not actually compare values when executed — there’s no need to, because the processor can already detect any modification to the data via cache-coherence messages. However, this comparison capability is still part of the instruction set, providing stricter guarantees for future or alternative implementations. The key question is: how does the core know that the data was overwritten between the LR and the SC? The answer is through coherence messages — specifically, those related to invalidation or revocation of ownership for the cache line. That’s exactly how it’s implemented in RISC-V BOOM, and architecturally, there’s simply no other mechanism for this. In fact, the LR/SC pair can be seen as the most fundamental form of software transactional memory (STM) support — a minimal building block that allows short, atomic transactions to be implemented efficiently.
Thank you very much for reading to the end. A word of warning — if this topic catches your interest and you decide to dig deeper, you’ll be stuck in it for at least half a year. It’s incredibly fascinating — and just as mind-bendingly complex.
This article was originally written in Russian *https://habr.com/ru/articles/895524/ *and translated into English with the help of ChatGPT!
Useful links:
https://github.com/tpn/pdfs — Memory Barriers — a Hardware View for Software Hackers (July 23, 2010)
If you want to dive into the source code, start here:
L1$D cache — https://github.com/riscv-boom/riscv-boom/blob/master/src/main/scala/v4/lsu/dcache.scala
Load/Store Unit — https://github.com/riscv-boom/riscv-boom/blob/master/src/main/scala/v4/lsu/lsu.scala
Instruction table — https://github.com/riscv-boom/riscv-boom/blob/master/src/main/scala/v4/exu/decode.scala
CPU core — https://github.com/riscv-boom/riscv-boom/blob/master/src/main/scala/v4/exu/core.scala
TileLink — https://static.dev.sifive.com/docs/tilelink/tilelink-spec-1.7-draft.pdf
https://riscv.org/wp-content/uploads/2017/05/riscv-spec-v2.2.pdf
메타데이터
- post_id
- 4e42a7647233
- slug
- understanding-memory-consistency-in-risc-v-caches-barriers-and-atomics-4e42a7647233
- url
- https://medium.com/@geneopenminder/understanding-memory-consistency-in-risc-v-caches-barriers-and-atomics-4e42a7647233
- canonical_url
- https://medium.com/@geneopenminder/understanding-memory-consistency-in-risc-v-caches-barriers-and-atomics-4e42a7647233
- author_url
- https://medium.com/@geneopenminder
- status
- ok
- fetched_at
- 2026-06-09 15:37:30