内存不是一块
目标
如果只抽象地学习计算机体系结构,很难真正留下印象。请直接测量这个 Pod 中的真实 CPU。
这里测得的数字,之后会成为“为什么这段代码这么慢”“为什么计算结果差了 1 韩元”等问题的答案。
查看位置
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
测量时间时
Python 很慢。因此必须固定读取次数,只改变数组大小,这样 Python 本身的慢速开销会被抵消,只留下内存差异。
另外务必先预热一次——第一次执行中会混入创建数组的成本。
步骤
- CPU 与缓存 →
01-cpu.txt - 缓存外延迟 →
02-latency.txt - 缓存行 →
03-line.md - 0.1 + 0.2 →
04-float.txt - 加法顺序 →
05-order.txt - 端序 →
06-endian.txt - 二进制补码 →
07-int.txt - 总结 →
08-notes.md
参考
第 2 步的数值因机器而异。请看倍数而不是绝对值——缓存内外相差超过两倍,就说明测量正确。
读取本机 CPU 与缓存信息
提取这台机器的 CPU 名称与缓存层级(L1/L2/L3 大小及缓存行大小),保存到 01-cpu.txt。
CPU 使用 grep -m1 'model name' /proc/cpuinfo 查看。缓存可读取 /sys/devices/system/cpu/cpu0/cache/index*/ 下的 level、type、size、coherency_line_size。
这一步要亲眼看到缓存像阶梯一样分成多层,且层级越高,容量越大、速度越慢。
测量离开缓存后会慢多少
保持读取次数相同,只改变数组大小,测量每次读取耗时并保存到 02-latency.txt。测试 4KB、1MB、64MB 三种大小即可。
关键是固定读取次数,这样 Python 自身的慢速开销会被抵消,只留下内存差异。
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
预先生成 30 万个随机索引,让三个数组使用相同次数。缓存内外的差异必须超过两倍。
理解顺序读取为何更快
确认缓存行大小,并计算若数组元素是 int32,一行能容纳多少个元素,写入 03-line.md。
缓存行大小已在第 1 步查看(通常为 64B)。int32 占 4 字节,因此一行可容纳 16 个。
内存不是一次取一个字节,而是整行读取。所以顺序读取数组时,一次取来的行中有 16 个元素可直接使用;随机读取则往往每次取新行,剩余 15 个都被浪费。
同一份数据仅靠改变访问顺序就能快上数倍,原因就在这里。
0.1 + 0.2 并不等于 0.3
把 0.1 + 0.2 的结果、== 0.3 的判断结果,以及将 0.1 输出到小数点后 20 位的值保存到 04-float.txt。
使用 print(f"{0.1:.20f}")。0.1 无法用二进制精确表示,就像十进制无法精确写出 1/3。
因此不能用 float 处理货币,应使用整数(最小货币单位)或十进制类型。
加法顺序会改变结果
使用相同三个数,只改变相加顺序,构造结果不同的示例并保存到 05-order.txt。
试试 1e16、1.0、-1e16。
1e16 + 1 - 1e16 = 0.0
1e16 - 1e16 + 1 = 1.0
把小数加到大数上时,小数会被挤出有效位而消失。因此浮点加法不满足结合律,求和时通常从较小的数开始相加更准确。
同一个数字,不同字节顺序
将 0x12345678 分别以小端序和大端序编码为 4 字节,保存到 06-endian.txt,并注明本机采用哪一种。
使用 struct.pack('<I', n).hex()、struct.pack('>I', n).hex(),以及 sys.byteorder。
小端序显示为反转后的 78563412。文件或网络传输数字时,如果端序不一致,值就会完全错误——网络字节序规定为大端序正是这个原因。
二进制补码与溢出
确认把 2**31 按有符号 32 位整数读取会得到什么,以及把 -1 按无符号 32 位整数读取会得到什么,并保存到 07-int.txt。
使用 struct.unpack('<i', struct.pack('<I', 2**31))[0],以及反向转换。
结果应为 2**31 → -2147483648、-1 → 4294967295。区别只在于如何解释同一组比特。
其他语言中整数突然变成负数的事故(溢出)就是这样发生的。Python 整数会自动扩展,但这是 Python 的特殊行为。
总结三个要点
在 08-notes.md 中至少写三行:为何访问缓存外更慢、为何不能用 float 处理货币,以及何时必须统一字节顺序。
正文中必须包含 캐시、소수、순서。