CASE STUDY / 02 / COMPUTER ARCHITECTURE
Same capacity. Different misses.
I wrote a small cache-memory model to find out exactly how memory-address mapping, associativity and replacement choices change observable behaviour.
THE PROJECT IN ONE MINUTE
My engineering work
Implemented a configurable Node.js cache model, repeatable traces and tests for address mapping, associativity and FIFO/LRU replacement.
Inspect first
Eight passing tests and generated results. On the 96-access synthetic conflict trace, the equally sized two-way model hits 94 times versus zero for direct mapping.
Verification boundary
Functional educational software model, not cache RTL, cycle timing or a CPU benchmark. The traces are deliberately synthetic.
Why isn't cache size the whole story?
Two caches can have the same capacity and cache-line size, yet return different hit rates for the same address sequence. The missing variable is where a block is allowed to live. A direct-mapped cache assigns one line to each set. A two-way associative cache can keep two competing blocks in a set at once.
I isolated this behaviour using tiny deterministic traces rather than attempting a full processor benchmark. That lets every reported hit and miss be reproduced—and makes the limitations easy to inspect.
Follow the address to its set.
In the experiment below, both caches hold 128 bytes with 16-byte lines. Addresses 0 and 128 belong to different memory blocks but map to the same set. With one way they continually evict each other; with two ways both can remain resident after the first two misses.
Every alternating access causes a conflict miss.
Both blocks fit; later accesses hit.
Block index
Divide the byte address by the line size, discarding the offset within the block.
Set index
Take the block index modulo the number of sets. Associativity changes the number of sets, even at constant capacity.
Tag and replacement
Use the remaining block-index bits as the tag. On a miss, take an unused way or replace the oldest entry under LRU or FIFO.
The trace makes the difference visible.
All values below were generated by running the checked-in program against fixed synthetic address sequences, with the same 128-byte capacity and 16-byte line size. The percentages are hit rates, not execution speedups.
| Trace | Accesses | Direct-mapped | 2-way LRU | 2-way FIFO |
|---|---|---|---|---|
| Sequential words | 128 | 75% | 75% | 75% |
| Competing addresses | 96 | 0% | 97.92% | 97.92% |
| Repeated working set | 96 | 95.83% | 95.83% | 95.83% |
| Replacement distinction | 5 | 20% | 40% | 20% |
Why does LRU beat FIFO on the same two-way set?
- Input
Byte addresses
0, 64, 0, 128, 0all map to set zero in the 128-byte, 16-byte-line, two-way model. The third access revisits block zero. - Decision
That hit makes block zero recently used for LRU, but does not change its insertion order for FIFO. When block 128 arrives, LRU evicts block 64; FIFO evicts block zero.
- Measured model output
The last access to zero hits under LRU but misses under FIFO. The generated five-access dataset reports 2/5 LRU hits versus 1/5 FIFO hits. This contrasts replacement rules only; it measures no memory latency or CPU speedup.
Input trace ↗Expected-outcome assertion ↗Generated result data ↗
python3 scripts/render_cache_terminal.py. See full-size output ↗Interpretation: associativity helps in the deliberately conflicting trace; the sequential trace already benefits from 16-byte lines. In the five-access replacement trace, revisiting a block updates LRU but not FIFO ordering. None of these experiments predicts speed on a real CPU.
8 / 8 tests passed
Node's built-in test runner checks address extraction, hit/miss outcomes, associativity, replacement order, input validation and replay determinism. A fixed-seed, 800-access test cross-checks eight configurations against a separate reference implementation.
Not a screenshot. Working code.
The site publishes readable copies of the original cache model, tests, experiment generator and result dataset. The backing portfolio GitHub repository currently requires access, so the links below use public, release-matched source snapshots instead of an inaccessible GitHub file viewer.
node --test projects/cache-lab/cache.test.mjs
node projects/cache-lab/experiments.mjs
# Add --write to regenerate results.json
Experiments are deterministic. No external services, API keys or dependencies are needed for the simulation.
Download the runnable cache source and tests ↓ (original folder names preserved; Node.js 20+). The model is public educational JavaScript, not CPU cache RTL.
A good model makes its boundaries clear.
This is a small functional teaching model, not synthesizable SystemVerilog and not a cycle-accurate simulation. It does not implement stores, dirty lines, coherence, prefetching, memory latency or pipeline behaviour. The traces are artificial by design, not application benchmarks.
Possible next steps are a write-policy model, real-world traces and then a small RTL cache-controller experiment. Those remain plans, not completed work.
What interested me most was how a simple design choice became a measurable and testable architectural tradeoff.
Discuss the project