The controller stalls the eviction and keeps the line in the array. Overwriting the victim and hoping the PutM is accepted later is not an option. Review Dirty Writeback for the message order; this lesson follows the WB entry's lifetime.
Same two-core model (MSI): X started at 0, C0 has stored 4 in M, C1 is I and memory = 0. The rows are checkpoints in one eviction, in the course's default order (PutM, H writes memory, Put-Ack). A WB entry is storage in C0's controller, not an extra cache. The eviction states are defined on the topic hub.
Step | C0 state / value | C1 state / value | H directory | Memory | Newest data |
|---|---|---|---|---|---|
No free WB entry | M / 4, eviction stalls | I / - | exclusive(C0) | 0 | C0: 4 |
WB entry reserved, victim moved | MI^A / 4 (in WB) | I / - | exclusive(C0) | 0 | C0 WB entry: 4, PutM(4) in flight to H |
PutM at H | MI^A / 4 (in WB) | I / - | exclusive(C0), pend PutM C0 | 0 | C0 WB entry and H buffer: 4 |
H writes memory, Put-Ack in flight | MI^A / 4 (in WB) | I / - | U | 4 | C0 WB entry and memory: 4 |
Put-Ack at C0, WB entry freed | I / - | I / - | U | 4 | memory: 4 |
A WB entry records the address, the data and whether Put-Ack has arrived. A forwarded request for X must not miss just because the array entry is gone. If a Fwd-GetS reaches C0 in MI^A, C0 sends Data from the WB entry and moves to SI^A, and if a Fwd-GetM arrives, C0 sends Data and moves to II^A. In both cases C0 frees the WB entry only on Put-Ack. H does not record C0 as a sharer after a Fwd-GetS answered from the WB entry, and it acks the stale PutM without writing memory (Part 4 shows how H knows C0 is no longer a holder). Part 4's lesson Eviction Versus Intervention works through these races.
NEWEST = 4
def run(free_wb_at_send):
# State after each step: (array, src_wb, h_buf, memory).
# Storage only: a PutM in flight cannot be resent or used to answer a Fwd-GetM.
array, src_wb, h_buf, memory = NEWEST, None, None, 0
steps = [("0 warm start", (array, src_wb, h_buf, memory))]
array, src_wb = None, array # L: evict into WB entry, MI^A
steps.append(("C0 evicts", (array, src_wb, h_buf, memory)))
msg = src_wb # send PutM
if free_wb_at_send:
src_wb = None # the bug
steps.append(("PutM in flight", (array, src_wb, h_buf, memory)))
h_buf = msg # 1: PutM at H
steps.append(("1 PutM at H", (array, src_wb, h_buf, memory)))
memory, h_buf = h_buf, None # H writes memory, sends Put-Ack
steps.append(("H writes memory", (array, src_wb, h_buf, memory)))
src_wb = None # 2: Put-Ack at C0 frees WB entry
steps.append(("2 Put-Ack at C0", (array, src_wb, h_buf, memory)))
return steps
good = run(free_wb_at_send=False)
assert all(NEWEST in state for _, state in good)
assert good[-1][1] == (None, None, None, NEWEST)
bad = run(free_wb_at_send=True)
gaps = [name for name, state in bad if NEWEST not in state]
assert gaps == ["PutM in flight"], gaps
assert bad[-1][1][3] == NEWEST # the end state looks correct, the gap is mid-flight
print("PASS: keeping the WB entry until Put-Ack keeps 4 stored at every step")The buggy policy still ends with memory = 4, which is why checking only the final state misses it. While its PutM is in flight, a Fwd-GetM reaching C0 would find no stored copy of 4 to send. The program models one eviction with no concurrency or finite queues.
In the course's default order, yes: H writes memory and then sends Put-Ack. In the early Put-Ack variant memory may still be 0, and H must keep 4 in its own buffer until the memory write is done.