A retired store waits in a FIFO buffer until it wins cache ownership. Later loads from any core can run before that store reaches memory.
The buffer is small (a few entries), so the reorder window is short. Short is enough: a racing load only needs to slip into that window.
A core's own load checks its buffer first and takes the buffered value on a match. The core never sees its own store late.
Other cores have no such path, so they still read the old memory value. That split is exactly the SB weak outcome.
Invalidation queues delay the processing of incoming invalidates. A load can briefly read a line that a remote store already won.
Speculative and out-of-order loads run ahead of program order. They expose LdLd and LdSt reorderings, which is the reader side of MP.
Source | Pairs it breaks | Pattern it enables | Fix |
|---|---|---|---|
FIFO store buffer | StLd | SB | Drain on a full fence (MFENCE, DMB, FENCE). |
Invalidation queue | StLd visibility | SB-like stale reads | Acknowledge before the fence retires. |
Speculative loads | LdLd, LdSt | MP reader side | Full fence or acquire load. |
Non-atomic store paths | global order | IRIW (historic only) | Multi-copy-atomic store release. |
The model below is a two-core FIFO store buffer of depth 4 with same-core forwarding. It runs the SB schedule buffered, then reruns it with stores retiring straight to memory as the SC control.
module tb_sb_storebuf;
localparam int DEPTH = 4;
int mem_x, mem_y;
int a_addr[4], a_data[4], a_cnt;
int b_addr[4], b_data[4], b_cnt;
int errors, r1, r2;
task automatic a_push(input int a, input int d);
a_addr[a_cnt] = a; a_data[a_cnt] = d; a_cnt = a_cnt + 1;
endtask
task automatic b_push(input int a, input int d);
b_addr[b_cnt] = a; b_data[b_cnt] = d; b_cnt = b_cnt + 1;
endtask
function automatic int a_load(input int a);
int i;
for (i = a_cnt - 1; i >= 0; i = i - 1)
if (a_addr[i] == a) return a_data[i];
if (a == 0) return mem_x;
return mem_y;
endfunction
function automatic int b_load(input int a);
int i;
for (i = b_cnt - 1; i >= 0; i = i - 1)
if (b_addr[i] == a) return b_data[i];
if (a == 0) return mem_x;
return mem_y;
endfunction
task automatic drain_all;
int i;
for (i = 0; i < a_cnt; i = i + 1) begin
if (a_addr[i] == 0) mem_x = a_data[i];
else mem_y = a_data[i];
end
for (i = 0; i < b_cnt; i = i + 1) begin
if (b_addr[i] == 0) mem_x = b_data[i];
else mem_y = b_data[i];
end
a_cnt = 0; b_cnt = 0;
endtask
initial begin
errors = 0;
mem_x = 0; mem_y = 0; a_cnt = 0; b_cnt = 0;
a_push(0, 7);
if (a_load(0) !== 7) errors = errors + 1;
if (mem_x !== 0) errors = errors + 1;
if (b_load(0) !== 0) errors = errors + 1;
drain_all();
if (mem_x !== 7) errors = errors + 1;
mem_x = 0; mem_y = 0;
a_push(0, 1);
b_push(1, 1);
r1 = a_load(1);
r2 = b_load(0);
drain_all();
if (r1 !== 0 || r2 !== 0) errors = errors + 1;
if (mem_x !== 1 || mem_y !== 1) errors = errors + 1;
mem_x = 0; mem_y = 0;
mem_x = 1;
r1 = mem_y;
mem_y = 1;
r2 = mem_x;
if (r1 !== 0 || r2 !== 1) errors = errors + 1;
if (r1 == 0 && r2 == 0) errors = errors + 1;
if (errors == 0) $display("PASS: forwarding holds, buffered SB shows r1=0 r2=0, direct SB shows r1=0 r2=1");
else $display("FAIL: %0d checks wrong", errors);
$finish;
end
endmoduleEach load missed the other core's still-buffered store and read the old memory value. That is the SB weak outcome.
B's load runs after A's store reached memory. Draining before the loads restores SC-visible order for this schedule.