62xL1 vs DRAM

A cache hit answers in about 1 nanosecond. The same load from RAM costs about 61.5. Reproduce every number below on your own machine.

Why is CPU cache faster than RAM? Proven in one number you can reproduce

A cache hit answers in about 1 nanosecond, DRAM in about 61.5. This is the causal chain behind that 62x gap, from the SRAM latch and the DRAM refresh stall to a benchmark you run yourself.

Why is CPU cache faster than RAM? One reproducible number#

Most explainers stop at "cache is closer and smaller". That is true, yet it explains nothing you can act on. Instead, start with the measurement and work backwards to its cause. On a Skylake i7-6700 an L1 hit costs about 1 nanosecond, and a DRAM read costs about 61.5. That single ratio is the whole thesis, and every section below earns it.

L1 cache hit versus DRAM access~62x slower
4 cycles

~1.0 ns

L1 cache hit

~62x slower

~61.5 ns

DRAM access

An L1 hit answers in about 1 nanosecond; the same load from DRAM costs about 61.5. That is roughly 62 times slower for identical work, measured on a 4.0 GHz Skylake i7-6700 by 7-cpu.

Show data table
L1 cache hit versus DRAM access (access latency (nanoseconds))
Optionaccess latency (nanoseconds)
L1 cache hit~1.0 ns
DRAM access~61.5 ns

The claim, and how the rest of this page proves it#

So why is CPU cache faster than RAM? It comes down to two independent penalties that add up. The first is the cell. The second is the distance. Because both are physical, both are measurable, and neither is a vendor talking point. The chain runs in five links. First the transistor, then the DRAM timing, next the on-die signal path, then a benchmark you run, and finally a table that maps each symptom to a fix. Read the map once and no later section is a surprise.

The latency ladder: cycles, nanoseconds, capacity, and bandwidth#

Before the physics, here is the ground truth every top result leaves scattered across ten pages. This table gathers the whole hierarchy for one named chip. Each row lists the latency in cycles and nanoseconds, the typical capacity, and a rough bandwidth. Read it top to bottom and the cost of distance is already visible.

The memory hierarchy on a Skylake i7-6700 at 4.0 GHz. Cycle and nanosecond figures are measured by 7-cpu; bandwidth is a single-thread order-of-magnitude, and one cycle at 4.0 GHz is about 0.25 ns.
LevelLatency (cycles)Latency (ns)Typical capacityRough bandwidth
Registerin-core~0.25 ns~2 KB register filereads every cycle
L1 cache4 cyc~1.0 ns32 KB per core~200 GB/s
L2 cache12 cyc~3.0 ns256 KB per core~80 GB/s
L3 cache42 cyc~10.5 ns8 MB shared~40 GB/s
DRAM42 cyc + 51 ns~61.5 nsgigabytes~10.8 GB/s

Reading each row: the physical cause behind the number#

That single table is the spine of why CPU cache is faster than RAM. Notice the shape of the jump. From L1 to L3 the latency grows about tenfold, yet it stays on-die. From L3 to DRAM it grows sixfold again, because the request now leaves the chip. Meanwhile the capacity moves the opposite way, from kilobytes to gigabytes. That trade is not an accident. It is forced by the storage cell, which is where the next section begins.

It starts at the transistor: SRAM's 6T latch vs DRAM's 1T1C cell#

The deepest reason why CPU cache is faster than RAM lives in the storage cell itself. Cache and RAM store a bit with different circuits, and that single choice sets everything downstream. So look at the two cells side by side. One is built for speed, the other for density, and you cannot have both in one cell.

  1. Cross-coupled latchTwo inverters wired back to back actively hold the stored bit. It stays put with no refresh, which is why an SRAM read is just a couple of cycles.
  2. Access transistorsA pair of transistors connect the latch to the bit lines. You read the held value directly, with nothing to reconstruct.
  3. Six transistors per bitThe whole cell costs six transistors. That is fast but large and power-hungry, so SRAM stays small: kilobytes, not gigabytes.
  4. Storage capacitorA single tiny capacitor holds the bit as charge. Charge leaks away in milliseconds, so the cell forgets unless it is refreshed.
  5. One access transistorOne transistor gates the capacitor onto the bit line. One transistor plus one capacitor is tiny, so DRAM packs gigabytes cheaply.
  6. Destructive readReading drains the capacitor, so the value must be written back every time, and every row must be refreshed on a timer.
Two ways to store one bit. The 6T SRAM latch on the left is read directly and never needs refreshing. The 1T1C DRAM cell on the right is dense and cheap, but leaky and destructive to read. The numbered notes carry the detail.

The 6T SRAM cell: a latch you read directly#

An SRAM cell is a latch. Two inverters feed each other, so the pair holds one stable bit as long as power is on. Because the bit is held actively, nothing has to be refreshed. Furthermore the value is read straight off the latch through two access transistors, with no reconstruction step. That is why a cache hit resolves in a handful of cycles. The cost is size. Six transistors per bit is bulky and power-hungry, so SRAM stays measured in kilobytes.

The 1T1C DRAM cell: one capacitor, and why it leaks#

A DRAM cell stores a bit as charge on one capacitor, gated by one transistor. One transistor plus one capacitor is tiny, so DRAM packs gigabytes onto a die cheaply. However that density has a price. Charge leaks away within milliseconds, so the controller must refresh every row on a fixed timer or the data rots. Moreover reading the cell drains the capacitor, which makes the read destructive. Consequently every read is followed by a write-back, and that hidden work is where the stall begins.

Where the stall is born: RAS, CAS, precharge, refresh#

Competitors say "DRAM is slower" and stop. Instead, watch the slowness happen. A DRAM read on a fresh row is not one action. It is a sequence: activate the row with a row address strobe (RAS), select the column with a column address strobe (CAS), move the bytes, precharge the row back, and share time with refresh. Step through it below and watch the roughly 51 nanosecond penalty accumulate, one phase at a time.

Step the DRAM row-miss stall as it accumulates to ~51 ns
0 / 5 phases
0 nsaccumulated of ~51 ns row-miss penalty
RAS14 nsActivate the row
CAS14 nsSelect the column
I/O5 nsDrive the data across the bus
tRP14 nsPrecharge to close the row
tRFC4 nsPeriodic refresh, amortized
Row miss

Why one DRAM read costs about 51 ns

A cache hit reads a latch and returns. A DRAM read cannot. It must activate a row, select a column, move the bytes across the bus, precharge the row back, and share time with periodic refresh. Step through the five phases and watch the 51 ns add up.

Each phase of a DRAM row miss below adds to the running total. Press Next to open the row.

Press Next to run each phase of a DRAM row miss. Each phase adds its nanoseconds to the running total. The figures are an illustrative DDR4 decomposition of the 51 ns adder 7-cpu measures on Skylake; they sum to 51 ns. With JavaScript off, the intro card and the labelled track state the full breakdown.

None of that work exists for a cache hit. An SRAM latch has no row to activate, no column to select, and no charge to restore. Therefore the DRAM penalty is not a bigger version of a cache access. It is extra machinery that a cache simply does not run.

Distance is latency: the on-die signal path#

Distance is the second half of why CPU cache is faster than RAM. Even at the speed of light, signals take time to cross a chip, and more time to leave it. So the physical layout of the parts sets a floor on latency that no cleverness removes. Trace the path from the execution units out to DRAM and the cost of each hop becomes concrete.

The on-die signal path from the ALU to DRAML1 sits beside the execution units, so a hit is nearly free. Each further level is physically farther and adds latency. Past L3 the request queues at the memory controller, arbitrates for the bus, leaves the die, and only then reaches DRAM, where the row activate and column read still wait.

Why L1 sits beside the ALU and DRAM sits across a bus#

L1 is tiny for a reason. A small array can sit right next to the execution units, so its wires are short and its lookup is quick. As the caches grow they must sit farther out, which is why L2 and L3 each cost more cycles. Past L3 the picture changes in kind, not degree. Now the request queues at the memory controller, waits its turn for the shared bus, and travels off-chip. Because that arbitration and travel are serial, they add cycles that on-die caches never pay.

Prove it on your machine: the latency-ladder benchmark#

Numbers you cannot reproduce are just claims. So here is the load-bearing part. A short pointer-chase benchmark measures access latency at every working-set size, and its output draws the same L1, L2, L3, and DRAM knees the table predicts. Build it, run it, and compare your knees to the plot.

The pointer-chase microbenchmark, line by line#

The trick is a dependent load. Each step reads a pointer that tells you the next address, so the CPU cannot prefetch ahead. As the working set grows past each cache, the average access falls out of that level and latency jumps. The strided variant does the opposite job. It exposes conflict misses by hammering the same cache set with a power-of-two stride. The third tab reads the cache topology straight from an Apple M1.

c
// latency.c -- a pointer-chase microbenchmark.
// Build: cc -O2 -o latency latency.c
// Run:   ./latency
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// One dependent load per step: p = *p. The chain is a single random cycle,
// so the prefetcher cannot guess the next address. We measure pure access
// latency, never bandwidth.
static double chase(size_t bytes, long iters) {
    size_t n = bytes / sizeof(void *);
    void **a = malloc(n * sizeof(*a));
    for (size_t i = 0; i < n; i++) a[i] = (void *)&a[i];

    // Sattolo's algorithm links every slot into one big cycle.
    for (size_t i = n - 1; i > 0; i--) {
        size_t j = (size_t)rand() % i;
        void *t = a[i]; a[i] = a[j]; a[j] = t;
    }

    void **p = &a[0];
    struct timespec t0, t1;
    clock_gettime(CLOCK_MONOTONIC, &t0);
    for (long i = 0; i < iters; i++) p = (void **)*p;
    clock_gettime(CLOCK_MONOTONIC, &t1);

    __asm__ volatile("" :: "r"(p)); // keep p live; defeat dead-code removal
    free(a);
    double ns = (t1.tv_sec - t0.tv_sec) * 1e9 + (t1.tv_nsec - t0.tv_nsec);
    return ns / iters;
}

int main(void) {
    for (size_t kb = 4; kb <= 512L * 1024; kb *= 2)
        printf("%8zu KB  %6.2f ns/access\n",
               kb, chase(kb * 1024, 50L * 1000 * 1000));
    return 0;
}

On the Skylake box the pointer chase prints about 1 ns while the working set fits L1, then rises through roughly 3 ns and 10.5 ns, and finally settles near 61.5 ns once the data spills to DRAM. In practice those four plateaus are the knees. They are the physical hierarchy, printed by your own terminal.

The knees you should see (and how to read them)#

The plot below is the interactive twin of that benchmark. Sweep the working-set slider and a marker walks the curve, naming the level and the latency at each size. Switch the CPU preset and the knees jump to that chip's cache sizes, because an Apple M1 has a 128 KB L1 and a 12 MB L2 rather than Skylake's 32 KB and 8 MB. This is where prediction meets measurement.

Sweep the latency ladder: working-set size versus access latency
FitsL2Latency~3 nsCycles12 cyc

Drag the working-set slider to walk the curve. Switch the CPU to move the knees.

Measured levels for Skylake i7-6700 @ 4.0 GHz (the no-JavaScript source of truth).
LevelFits up toLatency (cycles)Latency (ns)
L132 KB4 cyc~1 ns
L2256 KB12 cyc~3 ns
L38 MB42 cyc~10.5 ns
DRAMbeyond last cache42 cyc + 51 ns~61.5 ns
Drag the slider to move the working-set marker along the curve; switch the CPU preset to redraw the knees for Skylake or the Apple M1. Figures are measured (7-cpu, plus a local sysctl read on this M1). With JavaScript off, the Skylake curve and the level table below it carry the same numbers.

Same data, 165x slower: locality is the whole game#

Here is the payoff the physics was building toward. Take one 256 MiB array of 32-bit integers, which is far larger than the 8 MB L3. Sum it in order and the run is bandwidth-bound at about 24.9 ms. Then chase the same elements in a random dependent order and the run takes about 4.13 seconds. That is the same arithmetic, roughly 165 times slower, purely from destroying locality.

Same 256 MiB array, two traversal orders~165x slower
~0.37 ns/elem

24.9 ms

Sequential sum (locality kept)

~165x slower

4.13 s

Random pointer-chase (locality destroyed)

A sequential sum runs bandwidth-bound at ~0.37 ns per element, because the 64 B line plus the prefetcher serve 15 of every 16 accesses from L1. A random dependent chase pays the full ~61.5 ns DRAM latency each time, so identical work runs about 165 times slower.

Show data table
Same 256 MiB array, two traversal orders (wall-clock time (milliseconds))
Optionwall-clock time (milliseconds)
Sequential sum (locality kept)24.9 ms
Random pointer-chase (locality destroyed)4.13 s

Cache lines and prefetch: why sequential is nearly free#

Memory never moves one byte. It moves a whole 64-byte line. So when you read one integer, the next fifteen arrive with it, already in L1. Furthermore the hardware prefetcher spots a sequential pattern and fetches the next line before you ask. Because of those two effects, a sequential scan hits L1 about fifteen times out of sixteen, which is the 0.37 ns per element above. Random access defeats both, and you pay full DRAM latency on nearly every step.

Associativity and the power-of-two-stride conflict-miss cliff#

Associativity sounds abstract until a loop hits it. A cache maps each address to a small set of slots. When you walk memory with a power-of-two stride, many addresses fall into the same set, so they evict each other even though the array fits in the cache. That is a conflict miss, and it shows up as a latency cliff in the strided benchmark. The fix is small: offset the array or pad the stride so accesses spread across sets.

AoS vs SoA and false sharing, as measured slowdowns#

Data layout is a latency decision in disguise. Store an array of structs and a loop that reads one field still drags the whole struct through cache. Switch to a struct of arrays and that field becomes contiguous, so a single line now carries eight useful values. Compare the two below, then look at the magnitudes.

c
// Array of structs: each particle is 64 bytes, but the hot loop reads only x.
// Every step drags 56 bytes of cold fields into cache alongside the one it uses.
struct particle { double x, y, z, vx, vy, vz, mass, pad; };
struct particle *p = malloc(N * sizeof *p);

double sum = 0;
for (size_t i = 0; i < N; i++)
    sum += p[i].x;        // 1 useful field per 64 B cache line
Show data table
Access cost per element for one 256 MiB sweep, three ways
Item Access cost per element
Sequential (struct of arrays) 0.37 ns/element
Array of structs (1 field per line) 3 ns/element
Random pointer-chase 61.5 ns/element

Sequential and random are the measured 0.37 and 61.5 ns/element figures; the array-of-structs bar is an illustrative middle case, since reading one of eight fields wastes seven eighths of every 64 B line.

Figure Access cost per element for one 256 MiB sweep, three ways One 256 MiB sweep. The sequential and random figures are measured; the array-of-structs bar is an illustrative middle case.

False sharing is the multi-thread version of the same problem. Two threads that update separate counters on the same 64-byte line force the cache-coherence protocol to bounce that line between cores. As a result, adding threads makes the code slower, not faster. Pad each counter onto its own line and the contention disappears.

From symptom to fix: making the physics actionable#

Physics is only useful when it changes your code. So here is the translation layer no competitor provides. Match the symptom you actually observe to the level that binds you, then apply the fix. Each row is a pattern you have probably already hit.

From an observed symptom to the level that binds you to the concrete fix
Symptom you observeWhich level binds youThe fix in code
Column-major walk of a row-major array is slowDRAM (locality destroyed)Traverse in memory order; block or tile the loops so each tile fits L2
Random-key lookups hit a latency wallDRAM (dependent misses)Sort or batch by key; pack hot fields together as a struct of arrays
Adding threads makes a shared counter slowerCache coherence (false sharing)Pad each counter to its own 64 B line; aggregate per thread, combine once
A loop that fits L2 still stalls on a fixed strideL2 (conflict misses)Offset the array or pad the stride so accesses spread across sets

When NOT to reach for cache optimization#

Locality tuning is not free, and it is not always the answer. It makes code more complex, so spend it only where a measurement says the memory system is the bottleneck. Reach for it in the wrong place and you trade readable code for no speedup.

Further reading and sources#

Every figure here traces to a primary source. Ulrich Drepper's What Every Programmer Should Know About Memory is the canonical treatment of the SRAM and DRAM cells and the refresh timing. The measured latencies come from 7-cpu's Skylake and Apple M1 pages, which I cross-checked against a local sysctl read on this M1. For cache organization and prefetch behavior, Agner Fog's microarchitecture and instruction tables are the reference, alongside the Intel optimization manuals. For a feel of the orders of magnitude over time, Colin Scott keeps the Latency Numbers Every Programmer Should Know interactive.

So, why is CPU cache faster than RAM? Now you can answer it from your own terminal, not from a slogan. This sits next to the rest of the low-level work we write about. If dependent-load timing interested you, our walkthrough of how computers generate random numbers one decision at a time traces a similar hardware-to-API path. The same latency budgeting drives the network, which is why what happens in the milliseconds after you hit a URL is the companion read. And once the working set is a database, the same cache math explains many common MySQL performance bottlenecks and how to fix them.

Memory-bound code is quiet until a workload grows and a loop that used to fit cache suddenly does not. If you want a second set of hands profiling a hot path, redesigning a data layout, or tracking down where locality leaked out of a system, our software engineering team is glad to help. No pressure and no lock-in.

Talk to our software engineering team

Maulik Macwan

Software Engineer, Atyantik Technologies

Hi, I am a Senior Software Engineer at Atyantik, skilled in Laravel, Livewire, and Filament, with strong knowledge of Core PHP, WordPress, and CodeIgniter. Experienced in payment gateway and API integrations, including OpenAI APIs and Docker. I follow PSR-12 standards, use Git, and work in Agile development. A quick learner, problem solver, and reliable team player dedicated to delivering quality solutions.

More from Maulik MacwanPerformance optimizationHire backend developers

Keep reading