AMBA CHI · Module 2 · Coherency Protocol Foundations
The Dirty Data Concept
A cached line is clean when it matches memory and dirty when the cache has written it and memory has not caught up. That distinction decides everything about safety and efficiency. A dirty line holds the only copy of the newest value, so it must be written back to memory or handed to a new owner before it is ever lost — the writeback contract. A clean line may be dropped silently, because memory already has it. This chapter defines clean versus dirty precisely, states the writeback contract, shows the dirty handoff between caches, and builds the per-line tracker that enforces it across SystemVerilog, Verilog, and VHDL. The tracker here is representative, not a full cache controller.
Foundation13 min readAMBA CHIDirty DataWritebackClean vs DirtyCache CoherencyWriteback Contract
Module 2 · Chapter 2.6 · Coherency Protocol Foundations
Project thread — 2.5 named the owner and its duties. This chapter defines the one that matters most for safety: the writeback of dirty data. Chapter 2.7 then turns to the events — invalidations — that trigger it.
1. Learning Outcomes
By the end of this chapter you should be able to:
- Distinguish a clean line (matches memory) from a dirty line (memory is stale).
- Explain the writeback contract and why a dirty line may never be dropped silently.
- Identify the two ways to discharge the obligation — write back to memory, or hand off to a new owner.
- Trace the dirty window: when it opens on a write and closes on a writeback.
- Implement a representative per-line dirty/writeback tracker in SystemVerilog, Verilog-2001, and VHDL.
- Verify that a dirty line is never dropped without a writeback and that a clean line may be.
2. Why Should I Learn This?
Dirty data is where coherency meets durability. A dirty line is, for a while, the only place the newest value exists — memory has the old one. Get its handling wrong and a write silently vanishes: the classic "it worked in simulation, then a cache eviction ate my store" bug.
It is also where coherency earns its efficiency. Clean lines cost nothing to discard — memory already has them. Only dirty lines cost a writeback. Every eviction, snoop, and cache-to-cache transfer in CHI branches on one bit: is this line dirty?
3. Key Terms
4. Previous Chapter Connection
Chapter 2.5 established that exactly one agent owns a line and owes its writeback. It treated "the value" and "the writeback duty" as abstract. This chapter makes them concrete: the value is dirty — divergent from memory — and the writeback duty is the promise to reconcile it before the line disappears.
Ownership said who is responsible. Dirty data says what they hold and why the writeback cannot be skipped: because the dirty copy is the only newest value in the whole system.
5. Core Concept — clean, dirty, and the contract
Every cached line carries a dirty bit:
- Clean (dirty = 0): the copy matches memory. Reads are safe from memory. The line can be evicted silently — nothing is lost.
- Dirty (dirty = 1): the cache wrote the line; memory holds the stale old value. This copy is authoritative. It cannot be dropped without action.
A local write sets the bit (the line diverges from memory). A writeback clears it (memory catches up). Between the two is the dirty window — the span during which the cache holds the only newest value.
The safety rule:
The writeback contract. A dirty line that is about to be lost — evicted for space, or invalidated by a peer's write — must first be written back to memory or handed to a new owner. It is never dropped silently, or the newest value is lost forever.
The efficiency rule is the flip side: a clean line is dropped silently. That asymmetry — free to discard clean, must-reconcile dirty — is why caches track the dirty bit at all.
6. Engineering Mental Model — the only signed copy
A dirty line is the only signed original of a contract; memory holds an outdated draft.
- While you hold the signed original (dirty), you must not shred it. If you need the desk space (eviction), you first file it with records (writeback to memory) or hand it to a colleague who takes over custody (dirty handoff).
- A clean copy is just a printout of what records already have — you can bin it anytime; nothing is lost.
- The moment you file it (writeback), records are current and your copy becomes an ordinary printout (clean).
The dirty bit is the sticky note that says "signed original — do not shred." The writeback contract is the office rule that enforces it.
7. Engineering Diagram — two ways to discharge the obligation
Both arrows preserve the newest value; the forbidden third option — a silent drop — has no arrow because it is a bug, not a transition.
8. Worked Example — the dirty window
CPU0 caches line A, initially clean and equal to memory (A = 5). Trace the dirty bit and memory as CPU0 writes and later evicts.
| Step | Event | CPU0 copy | dirty | Memory | Note |
|---|---|---|---|---|---|
| 1 | (start) clean copy | 5 | 0 | 5 | matches memory |
| 2 | CPU0 writes A = 9 | 9 | 1 | 5 | dirty window opens — memory stale |
| 3 | CPU0 reads A | 9 | 1 | 5 | reads its own newest value |
| 4 | CPU0 evicts A | — (writeback) | 1→0 | 9 | contract: writeback first |
| 5 | line gone | — | — | 9 | memory current; safe to drop |
The dirty window is Steps 2–4 — the only span where the newest value (9) lives solely in CPU0's cache. The eviction at Step 4 is not free: the dirty bit forces a writeback before the line may go. Had the line been clean, Step 4 would drop it with no memory access.
9. Transaction Walkthrough — write, hand off, or write back
Trace a dirty line's obligation through the two legal endings, mapped onto the CHI cast. Representative behavioral flow, not a byte-level trace.
- CPU0 writes A. The store makes CPU0's copy diverge from memory: dirty = 1. The dirty window opens. CPU0 now owes a writeback (2.5's ownership duty, made concrete).
- Ending A — writeback to memory. CPU0 evicts A for space. The writeback contract fires: CPU0 sends the dirty value to memory via the Home Node to the Subordinate Node; memory becomes current; CPU0's line is now clean and may be dropped. Duty discharged to memory.
- Ending B — dirty handoff. Instead, a peer's request routes through the Home Node to CPU0, which forwards the dirty line cache-to-cache. The receiver becomes the new owner and inherits the dirty bit; memory is not touched. Duty handed off, not discharged — someone still owes the eventual writeback.
- The forbidden ending. CPU0 drops the line without either — the value
9is gone, memory still says5, and later readers are silently wrong. This is what the contract exists to prevent.
10. RTL / Hardware View — a per-line dirty/writeback tracker
A representative per-line tracker enforcing the contract. It follows one line through Invalid → Clean/Dirty → and, on release of a dirty line, a Writeback-Pending state that must complete before the line becomes Invalid. Behavioral and simplified: one event per cycle, one line, no data path.
// Representative per-line dirty / writeback tracker (educational, not a full
// cache controller). Enforces: a dirty line cannot be dropped without a
// writeback; a clean line may be dropped silently.
module line_writeback_tracker (
input logic clk,
input logic rst_n,
input logic fill_clean, // line loaded clean from memory (read-allocate)
input logic local_write, // CPU writes this line
input logic release_req, // eviction (space) or peer invalidation
input logic wb_done, // writeback acknowledged by memory
output logic [1:0] state,
output logic dirty, // cache holds the only newest value
output logic wb_req, // a writeback is owed before the line is dropped
output logic can_drop // a clean line is being dropped silently this cycle
);
localparam logic [1:0] INVALID = 2'b00, CLEAN = 2'b01, DIRTY = 2'b10, WB_PENDING = 2'b11;
logic [1:0] next;
always_comb begin
next = state;
can_drop = 1'b0;
unique case (state)
INVALID: if (local_write) next = DIRTY; // write-allocate: dirty now
else if (fill_clean) next = CLEAN; // read-allocate: clean
CLEAN: if (local_write) next = DIRTY; // clean -> dirty on a store
else if (release_req) begin
next = INVALID; can_drop = 1'b1; // clean: silent drop is safe
end
DIRTY: if (release_req) next = WB_PENDING; // dirty: MUST write back first
WB_PENDING: if (wb_done) next = INVALID; // memory now current
default: next = INVALID;
endcase
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) state <= INVALID;
else state <= next;
assign dirty = (state == DIRTY);
assign wb_req = (state == WB_PENDING);
endmoduleThe same behavior in Verilog-2001:
// Representative per-line dirty / writeback tracker (Verilog-2001).
module line_writeback_tracker (
input clk,
input rst_n,
input fill_clean,
input local_write,
input release_req,
input wb_done,
output reg [1:0] state,
output dirty,
output wb_req,
output reg can_drop
);
localparam INVALID = 2'b00, CLEAN = 2'b01, DIRTY = 2'b10, WB_PENDING = 2'b11;
reg [1:0] next;
always @(*) begin
next = state; can_drop = 1'b0;
case (state)
INVALID: if (local_write) next = DIRTY;
else if (fill_clean) next = CLEAN;
CLEAN: if (local_write) next = DIRTY;
else if (release_req) begin next = INVALID; can_drop = 1'b1; end
DIRTY: if (release_req) next = WB_PENDING;
WB_PENDING: if (wb_done) next = INVALID;
default: next = INVALID;
endcase
end
always @(posedge clk or negedge rst_n)
if (!rst_n) state <= INVALID; else state <= next;
assign dirty = (state == DIRTY);
assign wb_req = (state == WB_PENDING);
endmoduleAnd in VHDL:
-- Representative per-line dirty / writeback tracker (VHDL).
library ieee;
use ieee.std_logic_1164.all;
entity line_writeback_tracker is
port (
clk, rst_n : in std_logic;
fill_clean : in std_logic;
local_write : in std_logic;
release_req : in std_logic; -- eviction or peer invalidation
wb_done : in std_logic;
state : out std_logic_vector(1 downto 0);
dirty : out std_logic;
wb_req : out std_logic;
can_drop : out std_logic
);
end entity;
architecture rtl of line_writeback_tracker is
constant INVALID : std_logic_vector(1 downto 0) := "00";
constant CLEAN : std_logic_vector(1 downto 0) := "01";
constant DIRTY : std_logic_vector(1 downto 0) := "10";
constant WB_PENDING : std_logic_vector(1 downto 0) := "11";
signal cur, nxt : std_logic_vector(1 downto 0);
signal cdrop : std_logic;
begin
comb : process(cur, fill_clean, local_write, release_req, wb_done)
begin
nxt <= cur; cdrop <= '0';
case cur is
when INVALID =>
if local_write = '1' then nxt <= DIRTY;
elsif fill_clean = '1' then nxt <= CLEAN; end if;
when CLEAN =>
if local_write = '1' then nxt <= DIRTY;
elsif release_req = '1' then nxt <= INVALID; cdrop <= '1'; end if;
when DIRTY =>
if release_req = '1' then nxt <= WB_PENDING; end if;
when WB_PENDING =>
if wb_done = '1' then nxt <= INVALID; end if;
when others => nxt <= INVALID;
end case;
end process;
seq : process(clk, rst_n)
begin
if rst_n = '0' then cur <= INVALID;
elsif rising_edge(clk) then cur <= nxt; end if;
end process;
state <= cur;
dirty <= '1' when cur = DIRTY else '0';
wb_req <= '1' when cur = WB_PENDING else '0';
can_drop <= cdrop;
end architecture;All three model the identical rule: a clean line drops silently; a dirty line diverts through Writeback-Pending and cannot become Invalid until wb_done.
11. Timing View — the dirty window closing
CPU0 writes A, holds it dirty, then writes it back. Watch memory stay stale for the whole dirty window and catch up only at the writeback. Timing is representative — real latencies are not fixed cycle counts.
The dirty window — memory is stale until the writeback
6 cyclesThe MEM_A row makes the danger visible: any read routed to memory during cycles 1–4 would return the stale 5. That is exactly why a dirty line must be the read source (2.3) and must be written back before it is lost.
12. Verification View — a dirty line is never dropped silently
Three properties enforce the contract.
// Bind to line_writeback_tracker. Encodings: DIRTY=2'b10, WB_PENDING=2'b11.
// 1. Only a CLEAN line may be dropped silently.
property p_only_clean_drops;
@(posedge clk) disable iff (!rst_n) can_drop |-> (state == 2'b01);
endproperty
assert property (p_only_clean_drops);
// 2. Releasing a DIRTY line diverts to Writeback-Pending — never straight to Invalid.
property p_dirty_diverts;
@(posedge clk) disable iff (!rst_n)
(state == 2'b10) && release_req |=> (state == 2'b11);
endproperty
assert property (p_dirty_diverts);
// 3. The line stays in Writeback-Pending until the writeback completes.
property p_hold_until_wb;
@(posedge clk) disable iff (!rst_n)
(state == 2'b11) && !wb_done |=> (state == 2'b11);
endproperty
assert property (p_hold_until_wb);The system invariant is a scoreboard / reference-model rule:
The number of writebacks a line performs equals the number of times it left a dirty state to Invalid. A dirty eviction with no matching writeback is a lost update.
- What it proves: dirty data always routes through a writeback before the line disappears, and clean data never wastes one.
- What it does not prove: that the writeback data is correct, that a dirty handoff (cache-to-cache) correctly transferred the dirty bit (not modelled here), or memory-ordering across lines (Module 12).
- Bug signature when it fails:
can_dropasserted whilestate == DIRTY(a dirty line dropped silently — the DebugLab), or aDIRTY → INVALIDtransition with noWB_PENDINGbetween.
13. Testbench — write, evict-dirty (writeback), evict-clean (drop)
Deterministic stimulus; outputs sampled after the clock edge settles — no sampling race.
module tb_line_writeback_tracker;
logic clk = 0, rst_n;
logic fill_clean, local_write, release_req, wb_done;
logic [1:0] state;
logic dirty, wb_req, can_drop;
int errors = 0;
line_writeback_tracker dut (.*);
always #5 clk = ~clk;
task automatic ev(input logic fc, lw, rr, wd,
input logic [1:0] exp_state,
input logic exp_dirty, exp_wbreq, exp_drop,
input string tag);
logic dr;
fill_clean = fc; local_write = lw; release_req = rr; wb_done = wd;
#1;
dr = can_drop; // combinational — valid this cycle
if (dr !== exp_drop) begin
errors++; $display("FAIL [%s] can_drop=%b exp=%b", tag, dr, exp_drop);
end
@(posedge clk); #1;
fill_clean = 0; local_write = 0; release_req = 0; wb_done = 0;
if (state !== exp_state || dirty !== exp_dirty || wb_req !== exp_wbreq) begin
errors++;
$display("FAIL [%s] state/dirty/wb_req = %0d/%b/%b (exp %0d/%b/%b)",
tag, state, dirty, wb_req, exp_state, exp_dirty, exp_wbreq);
end else
$display("PASS [%s] state=%0d dirty=%b wb_req=%b can_drop=%b", tag, state, dirty, wb_req, dr);
endtask
initial begin
rst_n = 0; ev(0,0,0,0, 2'b00, 0,0,0, "reset"); rst_n = 1;
// Read-allocate clean, then a store makes it dirty.
ev(1,0,0,0, 2'b01, 0,0,0, "fill clean: -> CLEAN");
ev(0,1,0,0, 2'b10, 1,0,0, "write: CLEAN -> DIRTY");
// Evict a DIRTY line: must divert to WB_PENDING (no silent drop).
ev(0,0,1,0, 2'b11, 0,1,0, "release dirty: -> WB_PENDING (wb_req)");
if (can_drop) begin errors++; $display("FAIL: dirty line reported can_drop"); end
// Writeback completes: line becomes Invalid.
ev(0,0,0,1, 2'b00, 0,0,0, "wb_done: -> INVALID");
// Read-allocate clean again, then evict CLEAN: silent drop.
ev(1,0,0,0, 2'b01, 0,0,0, "fill clean: -> CLEAN");
ev(0,0,1,0, 2'b00, 0,0,1, "release clean: silent drop (can_drop)");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS [reset] state=0 dirty=0 wb_req=0 can_drop=0
PASS [fill clean: -> CLEAN] state=1 dirty=0 wb_req=0 can_drop=0
PASS [write: CLEAN -> DIRTY] state=2 dirty=1 wb_req=0 can_drop=0
PASS [release dirty: -> WB_PENDING (wb_req)] state=3 dirty=0 wb_req=1 can_drop=0
PASS [wb_done: -> INVALID] state=0 dirty=0 wb_req=0 can_drop=0
PASS [fill clean: -> CLEAN] state=1 dirty=0 wb_req=0 can_drop=0
PASS [release clean: silent drop (can_drop)] state=0 dirty=0 wb_req=0 can_drop=1
ALL TESTS PASSED14. DebugLab — a dirty line dropped on eviction
A dirty line dropped on eviction
DIRTY LINE DROPPED WITHOUT WRITEBACK -> LOST UPDATEUnder memory pressure, an occasional lost store: a value a core wrote is later read back as the old value, with no error. It reproduces only when the working set is large enough to force evictions.
The tracker trace at the failing eviction:
event state->next wb_req can_drop note
write A CLEAN->DIRTY 0 0 dirty window open (mem stale)
evict A (pressure) DIRTY->INVALID 0 1 (!) dropped like a clean line
later read A returns stale memory valueThe scoreboard flags a DIRTY → INVALID transition with no intervening WB_PENDING and no writeback — a lost update.
The eviction of A: the line went DIRTY → INVALID and asserted can_drop. That is the earliest wrong event — the value is already gone before any read observes it.
The eviction path did not branch on the dirty bit — it invalidated every victim the same way. For a clean line that is correct (memory has it); for a dirty line it discards the only newest value. The writeback contract was simply not enforced on the eviction path.
Branch eviction on dirty: a clean victim is dropped silently, a dirty victim is routed through Writeback-Pending and may not become Invalid until wb_done — exactly the DIRTY and WB_PENDING states of the tracker in Section 10. Do not "fix" it by writing every eviction back to memory; that wastes bandwidth on clean lines. Check the bit and write back only the dirty ones.
15. Common Mistakes
- Dropping a dirty line silently. Assumption: an eviction just invalidates the line. Bug: the only newest value is lost (the DebugLab). Prevention: dirty evictions route through a writeback; only clean lines drop silently.
- Writing back clean lines. Assumption: every eviction flushes to memory. Bug: wasted writeback bandwidth on lines memory already has. Prevention: write back only when
dirtyis set. - Reading a dirty line from memory. Assumption: memory is current. Bug: readers get the stale value during the dirty window. Prevention: source reads from the dirty owner (2.3), not memory.
- Losing the dirty bit on a handoff. Assumption: a cache-to-cache transfer copies only data. Bug: the receiver thinks the line is clean and later drops it silently — a lost update. Prevention: a dirty handoff transfers the dirty responsibility with the data.
- Clearing dirty before the writeback is acknowledged. Assumption: issuing the writeback is enough. Bug: if it is dropped or races, the value is lost with the line already marked clean. Prevention: clear dirty only on
wb_done. - Treating this tracker as a full cache controller. Assumption: four states are the whole story. Bug: missing tags, ways, coherence states, and the data path. Prevention: this is the dirty/writeback concept; the full controller is much larger.
16. Engineering Checklist
- Every line carries a dirty bit: set on a local write, cleared on
wb_done. - A clean line may be evicted silently; a dirty line may not.
- A dirty eviction or invalidation routes through a writeback (or a handoff) before the line is lost.
- Reads of a dirty line are sourced from the owner, never stale memory.
- A cache-to-cache handoff transfers the dirty responsibility with the data.
-
dirtyis cleared only after the writeback is acknowledged, never before.
17. Key Takeaways
- Clean = matches memory (droppable). Dirty = memory stale, this copy is the only newest value.
- The writeback contract: a dirty line must be written back or handed off before it is lost — never dropped silently.
- The dirty window opens on a write and closes on a writeback; during it, memory is stale.
- A dirty handoff moves the value and the writeback duty cache-to-cache, deferring — not cancelling — the writeback.
- Efficiency comes from the asymmetry: clean lines cost nothing to discard, only dirty lines cost a writeback.
- This tracker is representative — the dirty/writeback concept, not a full cache controller.
18. Quick Revision
Dirty data. A dirty bit per line: set on a local write, cleared on writeback-done. Clean = equals memory, droppable silently; dirty = memory stale, the only newest value. Writeback contract: a dirty line must be written back to memory or handed off to a new owner before eviction/invalidation — never dropped silently, or the write is lost. The dirty window (write → writeback) is when memory is stale; reads must come from the owner, not memory. A dirty handoff transfers the value and the writeback duty cache-to-cache (deferred, not cancelled). Representative model, not a full cache controller.
Coming Next
Chapter 2.7 — Invalidations. A dirty line's writeback is often triggered by another core wanting to write — an invalidation. The next chapter examines invalidations directly: when and why a cache must drop a copy, what event triggers a snoop-invalidate, how it interacts with the writeback contract, and why getting invalidation timing right is the heart of keeping every copy honest.