LabHub
学习 学习路径 课程

计算机组成

内存不是一块

在 LabHub 中继续学习

目标

如果只抽象地学习计算机体系结构,很难真正留下印象。请直接测量这个 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 本身的慢速开销会被抵消,只留下内存差异。

另外务必先预热一次——第一次执行中会混入创建数组的成本。

步骤

  1. CPU 与缓存 → 01-cpu.txt
  2. 缓存外延迟 → 02-latency.txt
  3. 缓存行 → 03-line.md
  4. 0.1 + 0.2 → 04-float.txt
  5. 加法顺序 → 05-order.txt
  6. 端序 → 06-endian.txt
  7. 二进制补码 → 07-int.txt
  8. 总结 → 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*/ 下的 leveltypesizecoherency_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

试试 1e161.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-14294967295。区别只在于如何解释同一组比特

其他语言中整数突然变成负数的事故(溢出)就是这样发生的。Python 整数会自动扩展,但这是 Python 的特殊行为。

总结三个要点

08-notes.md 中至少写三行:为何访问缓存外更慢、为何不能用 float 处理货币,以及何时必须统一字节顺序。

正文中必须包含 캐시소수순서