Cache Associativity: Direct-Mapped vs Set-Associative
Cache associativity determines where a memory block can be placed in a CPU cache. How direct-mapped, fully associative, and set-associative caches compare.
Cache associativity describes how many possible locations a given block of main memory can occupy within a CPU cache. It’s a design axis separate from cache size or level (L1, L2, L3): a cache can be small or large, close to the core or shared, and independently direct-mapped, fully associative, or somewhere in between.
The problem associativity solves
A cache is much smaller than main memory, so many memory addresses have to share a small number of cache lines. When the CPU wants to check whether an address is already cached, it needs a fast way to know where to look — checking every single cache line for every memory access would be far too slow to be useful.
Associativity is the answer to “how many places could this address possibly be, and how many places do I have to check.” The three main designs trade off lookup speed, hardware complexity, and how gracefully the cache handles memory access patterns that happen to collide.
Direct-mapped caches
In a direct-mapped cache, every memory address maps to exactly one specific cache line, usually determined by a set of bits from the middle of the address (the “index” bits). Lookup is trivial: compute the index, check that one line, and compare a tag to confirm it’s actually the address you wanted.
The simplicity is also the weakness. If two frequently-used addresses happen to map to the same line — which can happen even with plenty of free cache space elsewhere — they’ll repeatedly evict each other every time the program alternates between them. This is called a conflict miss, and it’s the associativity-driven counterpart to a capacity miss (where the cache is simply too small to hold everything the program is actively using).
Fully associative caches
At the other extreme, a fully associative cache lets any memory address be stored in any cache line at all. This eliminates conflict misses entirely — a line is only evicted when the cache is genuinely full and a replacement policy (commonly some approximation of least-recently-used) has to pick a victim among truly all the lines.
The cost is in the lookup hardware. Checking “could this address be in any of these lines” means comparing the requested tag against every single line’s tag in parallel, which requires much more comparator hardware than a direct-mapped design. This scales poorly as cache size grows, which is why fully associative caches are rare outside of small, specialized structures — a TLB is a common place you’ll actually find one, since TLBs are small enough that full associativity is affordable and the cost of a miss (a page table walk) is high enough to be worth avoiding.
Set-associative caches: the practical middle ground
Nearly every real CPU cache — L1, L2, L3 — uses set-associative design, which splits the difference. The cache is divided into a number of sets, and each memory address maps to exactly one set (like direct-mapped), but within that set, the address can go in any of a small fixed number of “ways” (like fully associative, but only among a handful of candidates rather than the whole cache).
A cache described as “8-way set-associative” has each set holding 8 lines, and an incoming address can occupy any of those 8 without regard to which specific way it lands in. This gets most of the conflict-miss resistance of full associativity — two colliding addresses can coexist as long as fewer than 8 things are simultaneously mapped to that set — while keeping the comparison hardware manageable, since lookup only needs to check the handful of ways within one set rather than the entire cache.
Comparing the three designs
| Direct-mapped | Set-associative | Fully associative | |
|---|---|---|---|
| Placement freedom | Exactly 1 possible line | N possible lines (one set) | Any line in the cache |
| Conflict misses | Most common | Reduced, not eliminated | Eliminated |
| Lookup hardware | Cheapest, single comparator | Moderate — N comparators per lookup | Most expensive — one comparator per line |
| Typical use | Rare in modern general-purpose CPUs | L1/L2/L3 caches in most CPUs | Small structures like TLBs |
| Scales to large caches | Yes, but with more conflict misses | Yes — most common real-world choice | Poorly — hardware cost grows fast |
Why this matters for real workloads
Associativity interacts directly with memory access patterns in ways that can be surprising. A program striding through an array with a stride that happens to be a multiple of the cache’s set count can end up hammering the same handful of sets while leaving the rest of the cache idle — degrading performance in a way that looks like a much smaller effective cache size than the hardware actually has. This is a known pathology in numerical and scientific code working with large matrices, and it’s part of why cache-conscious data layout and algorithms that respect memory locality (see the same instinct behind SIMD-friendly code) can produce large real-world speedups even when the underlying arithmetic hasn’t changed.
Associativity is also entangled with cache coherence in multi-core systems: the MESI protocol that keeps caches consistent across cores operates on cache lines, and how those lines are organized into sets and ways affects how coherence traffic is distributed across the cache. None of this is something application developers tune directly — it’s fixed in silicon — but understanding it explains why certain access patterns are fast and others, which look equivalent at the algorithm level, are measurably slower in practice.
What developers can actually do about it
You can’t choose your CPU’s associativity, but you can write code that behaves well regardless of it: favor sequential access over widely-strided access, keep frequently-used data structures compact enough to fit within cache capacity, and be skeptical of micro-benchmarks that don’t account for cache effects — a loop that looks identical to another can perform very differently depending on how the data it touches happens to map onto cache sets. This is the same reasoning that motivates a memory controller’s interleaving decisions and cache-level design generally: predictable, local access patterns are almost always faster than the algorithm’s big-O complexity alone would suggest.
The takeaway
Cache associativity determines how many places a given memory address can land in a cache: exactly one in a direct-mapped design, any line at all in a fully associative one, or a small fixed set of “ways” in the set-associative designs that dominate real CPUs. Set-associative caches exist because they capture most of full associativity’s resistance to conflict misses without its prohibitive comparison hardware cost — which is why nearly every L1, L2, and L3 cache you’ll encounter uses it, reserving full associativity for small, latency-critical structures like TLBs.
Tagged
Keep reading
Chisato · · 4 min read What Is HBM? High Bandwidth Memory Explained
HBM stacks DRAM dies vertically and connects them through a wide interface, trading capacity per chip for far higher bandwidth than standard DRAM.
Chisato · · 4 min read Thermal Interface Materials Explained
Thermal interface material fills microscopic gaps between a chip and its heatsink so heat can actually transfer to the cooler.
Chisato · · 4 min read What Is Dennard Scaling? Why Clock Speeds Stopped Climbing
Dennard scaling held that shrinking transistors kept power density constant, letting clock speeds rise for free. Its breakdown reshaped chip design.