TT Lab
Get started
Learn Learning paths Courses

Computer Architecture

Memory Is Not One Thing

Continue in TT Lab

Goal

If you learn computer architecture in the abstract, nothing sticks. Measure it directly on this Pod's real CPU.

The numbers you measure here will later become the answers to questions like "why is this code slow?" and "why is the calculation off by one won?"

Where to look

grep -m1 'model name' /proc/cpuinfo
cat /sys/devices/system/cpu/cpu0/cache/index0/size
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
nproc

When measuring time

Python is slow. So you must fix the number of reads and change only the array size, so that Python's slowness cancels out and only the memory difference remains.

And be sure to do one warm-up run — the first run includes the cost of building the array.

Steps

  1. CPU and cache → 01-cpu.txt
  2. Latency outside the cache → 02-latency.txt
  3. Cache line → 03-line.md
  4. 0.1 + 0.2 → 04-float.txt
  5. Order of addition → 05-order.txt
  6. Endianness → 06-endian.txt
  7. Two's complement → 07-int.txt
  8. Summary → 08-notes.md

Notes

The numbers in step 2 differ from machine to machine. Look at the ratio, not the absolute values — if inside and outside the cache differ by more than a factor of two, you measured correctly.

Read this CPU and its cache

Extract this machine's CPU name and its cache hierarchy (L1/L2/L3 sizes and line size), and save them in 01-cpu.txt.

For the CPU, use grep -m1 'model name' /proc/cpuinfo. For the cache, under /sys/devices/system/cpu/cpu0/cache/index*/, read level, type, size, and coherency_line_size.

This step is for seeing for yourself that the cache is a staircase of several levels, and that each level up is larger and slower.

How much slower does it get outside the cache

Keep the number of reads the same, change only the array size, measure the time per read, and save it in 02-latency.txt. Three sizes are enough: 4KB, 1MB, and 64MB.

The key is to fix the number of reads. That way Python's own slowness cancels out and only the memory difference remains.

import array, random, time
def bench(elems, idx):
    a = array.array('i', [1]) * elems
    for i in idx[:1000]: a[i]        # 워밍업
    t = time.perf_counter()
    s = 0
    for i in idx: s += a[i]
    return (time.perf_counter() - t) / len(idx) * 1e9

Build 300,000 random indices in advance and use the same count for all three arrays. The difference between inside and outside the cache must exceed a factor of two.

Why is reading in order faster

Check the cache line size, calculate how many elements of an int32 array fit in one line, and write it in 03-line.md.

You saw the line size in step 1 (usually 64B). int32 is 4 bytes, so 16 fit in one line.

Memory is fetched not one byte at a time but a whole line at a time. So if you read an array in order, you use 16 elements from a line you fetched once, for free. If you read randomly, you fetch a new line every time and throw away the other 15.

This is why changing only the access order makes the same data several times faster.

0.1 + 0.2 is not 0.3

Write down the result of 0.1 + 0.2, the verdict of == 0.3, and the value of 0.1 printed to 20 decimal places, and save them in 04-float.txt.

print(f"{0.1:.20f}"). 0.1 cannot be represented exactly in binary — just as 1/3 cannot be written exactly in decimal.

That is why you must not handle money as a float. Use integers (in won) or a decimal type.

The order of addition changes the result

Build an example in which adding the same three numbers with only the order changed gives different results, and save it in 05-order.txt.

Try 1e16, 1.0, and -1e16.

1e16 + 1 - 1e16 = 0.0
1e16 - 1e16 + 1 = 1.0

When you add a small number to a large one, the small one is pushed beyond the digits and vanishes. That is why floating-point addition is not associative, and why, when computing a sum, it is more accurate to add the smallest values first.

The same number, a different byte order

Make 0x12345678 into 4 bytes in little-endian and in big-endian, and save both in 06-endian.txt. Also note which one this machine uses.

struct.pack('<I', n).hex() and struct.pack('>I', n).hex(), and also sys.byteorder.

Little-endian appears reversed, as 78563412. If you do not match this when exchanging numbers through a file or the network, the values come out wrong — that is why the network byte order is fixed as big-endian.

Two's complement and overflow

Check what 2**31 becomes when read as a signed 32-bit integer and what -1 becomes when read as an unsigned 32-bit integer, and save it in 07-int.txt.

struct.unpack('<i', struct.pack('<I', 2**31))[0] and its reverse.

2**31 → -2147483648, and -1 → 4294967295. It is only a difference in how you decided to read the same bits.

This is what lies behind the incident in other languages where an integer suddenly turns negative (overflow). Python integers grow automatically, but that is Python being special.

Summarize three things

Write at least three lines in 08-notes.md: why access outside the cache is slow, why you must not handle money as a float, and when you must match the byte order.

The text must include 캐시, 소수, and 순서 (cache, decimal, order).