Unique capacity is the number of distinct lines the private caches and the LLC can hold together. The three policies trade it against snoop filtering and bandwidth:
Policy | Unique capacity | Snoop filtering | Costs | Good fit |
|---|---|---|---|---|
Inclusive | About the LLC size: every private line is duplicated in the LLC | Free: an LLC tag miss proves no private copy | Duplication, back-invalidation on LLC eviction, inclusion victims | LLC much larger than total private capacity, so duplication wastes little |
Exclusive | About total private capacity + LLC size | None from the LLC: needs a separate snoop filter | Every private eviction, clean ones too, costs an LLC fill (data-carrying clean eviction) | LLC not much larger than total private capacity, where duplication would waste a large share |
NINE | Between the two, depending on how much is duplicated | None from the LLC: needs a separate snoop filter | No back-invalidation from LLC data evictions, but the filter's evictions can still back-invalidate | Designers want to drop LLC lines freely without disturbing private caches |
Illustrative sizes, not a measured design: two private caches of 256 KiB each and a 1 MiB LLC. Inclusive gives about 1 MiB of unique data, exclusive about 1.5 MiB. The relative gap is total private capacity divided by LLC size, so it shrinks as the LLC grows relative to the private caches. The check also counts LLC fills per private eviction.
KiB = 1024
MiB = 1024 * KiB
def unique_capacity(policy, n_private, private, llc):
total_private = n_private * private
if policy == "inclusive":
if llc < total_private:
raise ValueError("inclusive LLC smaller than total private capacity")
return llc
if policy == "exclusive":
return total_private + llc
if policy == "nine":
return (max(llc, total_private), total_private + llc)
raise ValueError(policy)
def llc_fills(policy, clean_evictions, dirty_evictions):
if policy == "exclusive":
return clean_evictions + dirty_evictions
return dirty_evictions
incl = unique_capacity("inclusive", 2, 256 * KiB, 1 * MiB)
excl = unique_capacity("exclusive", 2, 256 * KiB, 1 * MiB)
assert incl == 1 * MiB and excl == 1.5 * MiB
low, high = unique_capacity("nine", 2, 256 * KiB, 1 * MiB)
assert incl <= low <= high == excl
naive_inclusive = 2 * 256 * KiB + 1 * MiB
assert naive_inclusive != incl
gaps = []
for llc in (1 * MiB, 2 * MiB, 4 * MiB, 8 * MiB):
a = unique_capacity("inclusive", 2, 256 * KiB, llc)
b = unique_capacity("exclusive", 2, 256 * KiB, llc)
gaps.append((b - a) / a)
assert gaps == sorted(gaps, reverse=True) and gaps[0] == 0.5 and gaps[-1] == 0.0625
try:
unique_capacity("inclusive", 2, 256 * KiB, 256 * KiB)
raise AssertionError("inclusion cannot hold with a smaller LLC")
except ValueError:
pass
assert llc_fills("exclusive", 80, 20) == 100
assert llc_fills("inclusive", 80, 20) == 20
assert llc_fills("nine", 80, 20) == 20
print("PASS: inclusive 1 MiB vs exclusive 1.5 MiB, gap shrinks as LLC-to-private ratio grows")Workload matters on top of capacity. A line read by both cores occupies two private entries under every policy, so exclusive does not remove that duplication. A streaming workload that touches each line once fills an exclusive LLC with clean victims nobody rereads, paying LLC fill bandwidth for nothing. A small hot working set that hits in L1 exposes an inclusive LLC to inclusion victims. When comparing policies, keep line size, private capacities and the workload fixed, and count the snoop filter's storage against exclusive and NINE.
An inclusive LLC causes inclusion victims through back-invalidation, and exclusive or NINE designs can still lose private lines to snoop filter evictions. LLC data capacity alone does not describe either.