Every variable in a running program lives in RAM, a long row of numbered boxes that each hold one byte. The CPU asks for a box by its number, its address, and the byte comes back. This page shows how a number spreads across several boxes, how far away those boxes are in time, and why visiting them in the wrong order can make the same loop many times slower. It builds on C00 · Bits & bytes and C01 · The CPU.
Below are 64 bytes of a toy memory, starting at address 0x1000 (hex). Real programs get much larger addresses from the operating system, but the rules are the same. Four C variables live here: a 4-byte integer x, an array of four 4-byte integers arr, an 8-byte pointer p, and an 8-byte string tag. Click any box to read it.
How to read this: Each box is one byte, written as two hex digits. Its address is the row label plus the column number (+0 to +7). The small grey word inside names the variable that owns the byte. The row of circles shows the selected byte as 8 bits.
The tiny corner tags mark the LSB (least significant byte, the small end of the number) and the MSB (the big end).
Try this: Press Store x, then switch to Big-endian and watch LSB and MSB trade places. Then set Index i to 5 and press Read arr[i].
Compile with cc demo.c && ./a.out. The compiler picks the real addresses, so only the byte order, offsets and values match the toy memory.
A 32-bit integer needs 4 bytes, and the CPU has to pick an order. Little-endian puts the least significant byte at the lowest address. x86 and almost all ARM systems work this way. Big-endian puts the most significant byte first, the way we write numbers. Network protocols traditionally use it and call it network byte order. A string is a sequence of single bytes, so byte order never changes it.
C does not check array bounds. arr[5] compiles, computes 0x1010 + 5 × 4, and reads whatever lives there. The C standard calls this undefined behavior. Python checks every index and raises IndexError instead.
The CPU can only compute on values in its registers. Everything else has to be fetched, and each level further out is bigger, cheaper per byte, and slower. The figures are typical round numbers for a recent desktop or laptop. Real machines differ by a factor of two or more, so read them as orders of magnitude. The bars use a log scale: each grid line is 10× slower than the one before.
How to read this: Each bar is how long the CPU waits for data from that place. The dot travels out from the CPU and back.
Try this: Press Visit every level, then switch to Human scale.
Each bit of main memory (DRAM) is a tiny capacitor next to one transistor. A charged capacitor reads as 1. The charge leaks away, so the memory controller reads and rewrites every row on a fixed schedule: at least once every 64 ms for DDR4, 32 ms for DDR5. Cut the power and the refreshing stops, the charge drains, and the bits fade to 0. Caches use SRAM, which holds bits in a loop of transistors with no refresh, but it also loses them without power. SSDs trap charge behind insulation, so they keep data for years unpowered.
How to read this: Each box is one bit's capacitor. The fill shows how much charge is left. Above the dashed line it reads as 1.
Toy model, time stretched: one refresh pass on screen stands for one 64 ms refresh window.
When the CPU misses in its cache, it never fetches just the value it asked for. It fetches the whole line around it, 64 bytes on most current x86 and ARM chips. If the next access is a neighbor, it is already there. C stores a 2D array row after row, so walking along a row uses every byte of each line. Walking down a column jumps a full row each step and can throw lines out before their neighbors get used.
How to read this: Each square is one number in the table. Memory stores them row after row. On a miss the cache copies a whole strip of neighbors at once.
How to read this: This strip is the cache itself. Each square holds one line copied from memory. Squares are grouped into small bins called sets, and each bin holds as many lines as there are ways. A line may only go in its own bin, so a full bin forces an older line out.
Try this: Press Play in Row by row and note the hit rate. Then switch to Column by column and play again. The grid turns red.
This is one small cache with LRU replacement (evict the least recently used line in the set), a cold start, and every miss priced at the lab 2 RAM figure. The array starts on a line boundary. Real CPUs stack L1, L2 and L3, and their hardware prefetchers spot steady strides and fetch ahead. That often shrinks the column-order penalty on real machines, but for arrays much larger than the cache it rarely disappears.
Row-major order is a C convention. NumPy uses it by default too, while Fortran, MATLAB and Julia store arrays column by column, so there the fast loop order flips. Plain Python lists hold pointers to separate objects, which is why this lab is written in C.