3.1Problems with Shared Memory Behavior

book pp. 17–20 · ~4 min read

  • store-store reordering
  • load-load reordering
  • store-load reordering
  • write buffer
  • non-determinism

Where this chapter goes

This chapter defines memory consistency models properly and then develops the most intuitive one — sequential consistency — all the way from Lamport’s definition (§3.4) and a reusable formalism (§3.5) to naive operational implementations (§3.6), a coherence-based implementation (§3.7), aggressively optimized ones (§3.8), atomic operations (§3.9), and a real machine, the MIPS R10000 (§3.10). But first it must convince you there is a problem at all. Throughout, “core” means software’s view of a core — a real core or a thread context of a multithreaded core — and all examples assume every variable starts at zero.

Example 1: the flag that lies

Core C1 writes some data, then sets a flag; core C2 spins until it sees the flag, then reads the data. Surely r2 must be NEW?

Table 3.1: should r2 always be set to NEW?

Core C1Core C2
S1: data = NEW;L1: r1 = flag;
S2: flag = SET;L2: r2 = data;

Initially data = 0, flag = 0. Table 3.1: should r2 always be set to NEW?

Model:

Possible outcomes (r1, r2) under Custom reorderings:

(SET, NEW)SC
1 execution — show oneS1 → S2 → L1 → L2

Only executions where C2's spin loop has exited (r1 = SET) are shown — the book's L1/B1 loop repeats until flag is SET, so iterations that read 0 just retry.

On some of today’s systems, r2 can be 0. Hardware can reorder C1’s stores S1 and S2: locally the reordering looks harmless — they access different addresses — but C2 can then observe flag = SET while data is still 0 (execution order S2, L1, L2, S1). Flip the Store→Store toggle above and watch the (SET, 0) outcome appear. Then try Load→Load instead: reordering C2’s loads produces the same disaster, and is even more plausible if the branch of the spin loop is elided.

One recorded execution of this kind (the book’s Table 3.2), with the coherence state of each block alongside:

CycleCore C1Core C2Coherence state of dataCoherence state of flag
1S2: flag = SETread-only for C2read-write for C1
2L1: r1 = flagread-only for C2read-only for C2
3L2: r2 = dataread-only for C2read-only for C2
4S1: data = NEWread-write for C1read-only for C2

Look at the state columns: the SWMR invariant holds at every cycle. This execution satisfies coherence perfectly. Incoherence is not the culprit — ordering is.

Example 2: Dekker’s disappointment

The core of Dekker’s mutual-exclusion algorithm: each thread announces itself with a store, then checks whether the other announced.

Table 3.3: can both r1 and r2 be set to 0?

Core C1Core C2
S1: x = NEW;S2: y = NEW;
L1: r1 = y;L2: r2 = x;

Initially x = 0, y = 0. Table 3.3 (Dekker-inspired): can both r1 and r2 be set to 0?

Model:

Possible outcomes (r1, r2) under Custom reorderings:

(0, NEW)SC
1 execution — show oneS1 → L1 → S2 → L2
(NEW, NEW)SC
4 executions — show oneS1 → S2 → L1 → L2
(NEW, 0)SC
1 execution — show oneS2 → L2 → S1 → L1

Intuition says three outcomes: (0, NEW), (NEW, 0), or (NEW, NEW). Surprisingly, most real hardware — including x86 systems from Intel and AMD — also allows (0, 0), because FIFO write buffers let each core’s load slip past its own buffered store. Enable Store→Load above to see it. And again: every one of these executions, including (0, 0), satisfies cache coherence.

Why we can’t just shrug this off

Two objections the book preempts:

  • “This is non-deterministic and confusing.” All current multiprocessors are non-deterministic by default: every architecture permits multiple interleavings of concurrent threads. The illusion of determinism is created — sometimes — by software synchronization. A definition of shared memory behavior must embrace non-determinism.
  • “Just define behavior for correct programs.” Memory behavior is usually defined for all executions of all programs — including incorrect ones and intentionally subtle ones (e.g., non-blocking synchronization algorithms). (Chapter 5’s high-level-language models will carve out a deliberate exception: executions with data races may get undefined behavior.)

Without a precise definition of what hardware may do, nobody can say which of these reorderings are bugs and which are features. That definition is the memory consistency model — next section.

Check yourself

1.In Table 3.1's program, what hardware mechanism can make r2 read 0 even though C1 stored data before setting the flag?

2.For Table 3.3's Dekker-style program, which outcome do real x86 systems permit that intuition forbids — and via what mechanism?

3.Do the surprising executions of §3.1 (r2 = 0; both registers 0) violate cache coherence?

4.Why can store-load reordering occur even on a core that executes all instructions strictly in program order?

5.Try it in the Table 3.1 widget: keep C1's stores in order, but enable Load→Load reordering (C2). Can r2 still read 0?

5 questions