Retention lowers the array supply far enough to keep each bit but not to read or write it, and switches off the controller logic around it. The contents survive; the ability to look up a tag and answer an Inv or Fwd does not. Coherence needs answers, not bits. So a retained cache has exactly two safe choices: leave the domain first (flush and Leave, as in the previous lessons), or stay in it and wake up whenever a snoop arrives.
Course-model extension for this topic: C0's cache can be powered down. A powered-off or retained controller answers nothing, not even 'I do not have it'. The topic adds four course-defined messages, Leave, Leave-Ack, Join and Join-Ack, for leaving and rejoining the coherence domain; they are not messages of any named protocol. Everything else follows the course model.
An always-on snoop port stays powered. It accepts an Inv or Fwd into a reserved slot, raises a wake request to the power controller, and back-pressures further forwards (it never answers 'absent'). When the array and controller are back, the held message is processed normally and the cache returns to retention after an idle timer. Two rules make it safe: the held snoop sits in the forward class and its response in the response class, so it cannot queue behind core requests; and the wake completes in bounded time, which is now part of every requester's progress assumption.
MSI. Warm-up: C0 acquired M on X and stored 1, then entered retention with wake-on-snoop and no flush. C1 sends GetS. Once C0 is awake, the remaining steps are the MSI dirty read of MSI Protocol: Data to H, H writes memory, Data to C1. Only the new part is shown.
Step | C0 state / value | C1 state / value | H directory | Memory | Newest data |
|---|---|---|---|---|---|
0. In retention | M / 1 (retained) | I / - | exclusive(C0) | 0 | C0: 1 |
1. GetS at H | M / 1 (retained) | IS^D / - | exclusive(C0), pend GetS C1 | 0 | C0: 1 |
2. Fwd-GetS held at C0's port | M / 1 (retained, waking) | IS^D / - | exclusive(C0), pend GetS C1 | 0 | C0: 1 |
Wake complete, Fwd-GetS processed | S / 1 | IS^D / - | exclusive(C0), pend GetS C1 | 0 | C0: 1, Data(1) in flight to H |
3. Data at H | S / 1 | IS^D / - | exclusive(C0), pend GetS C1 | 0 | C0 and H buffer: 1 |
H writes memory | S / 1 | IS^D / - | S{C0}, pend GetS C1 | 1 | C0, H buffer and memory: 1 |
C1's miss is longer by the whole wake time: retention moves a cost from C0's leakage to C1's latency (compare Anatomy of a Miss Latency). The wake rate depends on how precise H's records are: with a full directory only lines C0 really holds wake it; with broadcast every snoop does.
Option | Entry | While idle | Exit | Snoops while idle |
|---|---|---|---|---|
Full power-off | Flush and Leave | Leakage near zero | Power-up, Join, cold misses | None: C0 is not in the domain |
Retention with flush and Leave | Flush and Leave | Retention leakage | Faster wake, Join, but every line is I, so the same cold misses | None |
Retention with wake-on-snoop | Nothing to flush | Retention leakage plus the always-on port | Fast wake, cache still warm | Each one wakes C0 and delays its requester |
Retention with flush keeps bits that are not coherent: after Leave they must be treated as I, so it saves only state outside coherence (configuration, replacement history) and a faster power-up. Its niche is short idle periods with a busy snoop stream. The numbers below are illustrative, not measurements of any product.
# Illustrative numbers only: energy in microjoules, idle time T in ms, snoop rate r per ms.
FLUSH, REFILL = 2.0, 2.0 # write back dirty lines, refetch the working set
WAKE_OFF, WAKE_RET = 1.0, 0.3 # power-up from off, wake from retention
P_RET = 2.0 # retention leakage, uJ per ms (2 mW)
SNOOP_WAKE = 0.3 # wake, answer one snoop, return to retention
def energy(option, T, r, count_snoops=True):
if option == "off":
return FLUSH + WAKE_OFF + REFILL
if option == "retention, flush and leave":
return FLUSH + P_RET * T + WAKE_RET + REFILL
wakes = r * T if count_snoops else 0
return P_RET * T + WAKE_RET + wakes * SNOOP_WAKE # retention, wake on snoop
OPTIONS = ["off", "retention, flush and leave", "retention, wake on snoop"]
def best(T, r, count_snoops=True):
return min(OPTIONS, key=lambda o: energy(o, T, r, count_snoops))
# Each option wins somewhere.
assert best(0.5, 1) == "retention, wake on snoop" # short idle, few snoops
assert best(10, 1) == "off" # long idle
assert best(0.2, 100) == "retention, flush and leave" # short idle, many snoops
# General claim: beyond the break-even time, power-off wins at any snoop rate.
t_even = (FLUSH + WAKE_OFF + REFILL - WAKE_RET) / P_RET
for T in [t_even * k for k in (1.01, 2, 5, 50)]:
for r in [0, 1, 10, 1000]:
assert best(T, r) == "off", (T, r)
# Broken accounting: ignoring snoop wake-ups picks wake-on-snoop for a busy line.
naive = best(0.2, 100, count_snoops=False)
assert naive == "retention, wake on snoop"
assert energy(naive, 0.2, 100) > energy(best(0.2, 100), 0.2, 100)
print("PASS: break-even %.2f ms, and ignoring snoop wake-ups picks the wrong state" % t_even)With these illustrative values, power-off wins for any idle period longer than about 2.35 ms whatever the snoop rate, wake-on-snoop wins for short quiet periods, and retention with flush wins only for idle periods under about 0.35 ms that would see more than about 13 snoops (r x T > 13.3, at T = 0.2 ms more than 67 per ms). The broken accounting, which ignores the cost of each wake, picks wake-on-snoop for a busy line and spends 6.7 µJ instead of 4.7 µJ. A real choice also weighs C1's added latency, which this energy model leaves out.
No. After Leave-Ack H no longer tracks or invalidates C0, so a line C0 kept may have been written by C1 since. The retained bits must be treated as I and refetched; only state outside coherence can be reused.