SC’s implementations, plus buffers
The implementation story for TSO is chapter 3’s story plus per-core FIFO write buffers. Update the switch of §3.6 (the book’s Figure 4.4a):
- Loads and stores leave each core in its program order <p.
- A load either bypasses a value from the write buffer or awaits the switch as before.
- A store enters the tail of the FIFO write buffer — or stalls the core if the buffer is full.
- When the switch selects core Ci, it performs either Ci’s next load or the store at the head of Ci’s write buffer.
Figure 4.4 (recreated): two TSO implementations. Blue boxes are the new per-core FIFO write buffers — loads take the direct path and may bypass from the buffer; stores queue until memory (a) or the coherent memory system (b) accepts them. Everything else is chapter 3’s SC story.
And exactly as in §3.7, the switch can then be replaced by a cache-coherent memory system (Figure 4.4b), with everything from §3.8 — speculation, multithreading, non-binding prefetching — carrying over. Indeed, most real TSO implementations seem to be just that: take an SC implementation and insert write buffers. (How speculative cores physically organize them — combined store queue + write buffer, separate load/store queues — is beyond this chapter’s scope.)
One subtlety: on a multithreaded core, TSO write buffers are logically private to each thread context. A shared physical buffer tags entries with thread-context identifiers and permits bypassing only when the tags match.
4.4.1 Implementing atomic instructions
Think of the RMW as a load immediately followed by a store. The load part cannot pass earlier loads (TSO keeps Load→Load). Could it pass earlier stores waiting in the write buffer? No — by an indirect argument: the RMW is an atomic pair, so if its load passed an earlier store, its store would have to pass that store too, and Store→Store reordering is forbidden.
The implementation consequences:
- The RMW effectively drains the write buffer before performing its load part.
- The load part needs read-write coherence permission (not the read permission ordinary loads need), so the store part can be ordered immediately after it.
- The cache controller holds the block between the load and store parts, as in §3.9.
Optimized implementations skip the drain when (a) every buffered entry already has — and keeps — read-write permission in the cache until the RMW commits, and (b) the core performs R10000-style load-speculation checking (§3.8). All the earlier stores and loads then logically commit as a chunk immediately before the RMW.
4.4.2 Implementing FENCEs
TSO doesn’t order a store before a later load — when the programmer needs that order, a FENCE fence Instruction forcing all program-order-earlier memory operations into memory order before all later ones (a.k.a. memory barrier). defined in Chapter 4 — open in glossary between them supplies it. Watch the FENCEs kill the (0, 0) outcome operationally:
1 / 7Initial state
Memory starts as x = 0, y = 0; both write buffers are empty.
And confirm across all interleavings — with the FENCEs in place, TSO yields exactly the three SC outcomes:
Table 4.5: Dekker with FENCEs
| Core C1 | Core C2 |
|---|---|
| S1: x = NEW; | S2: y = NEW; |
| F1: FENCE | F2: FENCE |
| L1: r1 = y; | L2: r2 = x; |
Initially x = 0, y = 0. Table 4.5: the Dekker core with FENCEs — can both r1 and r2 still be set to 0?
Possible outcomes (r1, r2) under TSO (write buffers):
5 executions — show one
S1▸buf → S1▸mem → F1 → L1 → S2▸buf → S2▸mem → F2 → L260 executions — show one
S1▸buf → S1▸mem → F1 → S2▸buf → S2▸mem → L1 → F2 → L25 executions — show one
S1▸buf → S2▸buf → S2▸mem → F2 → L2 → S1▸mem → F1 → L1TSO enumeration is operational: ▸buf = store enters the FIFO write buffer, ▸mem = it drains to memory, (bypass) = a load read its own core's buffered store.
Because TSO permits only one type of reordering, FENCEs are infrequent, and a simple implementation — drain the write buffer at the FENCE, and stall later loads until the FENCE commits — may provide acceptable performance. For the relaxed models of chapter 5, where FENCEs are everywhere, their implementation becomes performance-critical.
Check yourself
1.In the write-buffered switch implementation (Fig 4.4a), what happens when the switch selects core Ci?
2.Flashback to pop-quiz question 4: on a multithreaded TSO core with a physically shared write buffer, when may a thread bypass a buffered value?
3.Why can't the load part of a TSO RMW pass earlier stores sitting in the write buffer?
4.Under what conditions can an optimized TSO implementation perform an RMW WITHOUT draining the write buffer?
5.Why is a simple FENCE implementation (drain the buffer, stall later loads until the FENCE commits) acceptable for TSO?