🐧 第六章 存储层次
📄 一句话概括

从寄存器到机械盘,一次访问的延迟差了几百万倍。把 L1 命中换算成 1 秒,内存要等 40 秒,SSD 要等 11 小时,机械盘要等 46 天。所以每多一次磁盘访问,代价等于让人等半天。

📝 本节和前面几节的关系

6.1 到 6.13 讲完了每一层,这一节把它们排成一张表,并给出一个【能记住一辈子】的比例尺。

一、全表的量级

寄存器:几百字节,不到 1 纳秒。 L1:64 KB,约 1 纳秒。L2:512 KB,约 4 纳秒。L3:8 MB,约 15 纳秒。 内存:32 GB,约 40 纳秒(不同资料给 40~100,量级一致就行)。 SSD:1 TB,约 40000 纳秒(40 微秒)。 机械盘:4 TB,约 4000000 纳秒(4 毫秒)。 注意最后两行的数量级:SSD 是内存的几千倍,机械盘是几百万倍。这不是【慢一点】,是【差一个世界】。

二、换成人的时间尺度

假设一次 L1 命中 = 1 秒。 那么 L2 = 4 秒,L3 = 15 秒,内存 = 40 秒。 SSD = 11 小时,机械盘 = 46 天。 所以【内存里多绕一圈】相当于让人多等几十秒,还能忍;【磁盘上一次 IO】相当于让人等半个月。 这就是为什么【缓存命中率】值得花大力气优化——每提高一点,省下的都是【半天】。

三、整章在讲什么

缓存(6.1~6.6):让重复访问落在快的那一层。 TLB(6.7):让地址翻译不用每次都查表。 页缓存(6.8):让文件访问不必每次去磁盘。 缓冲与批量(6.9~6.12):把多次小访问攒成一次大访问。 调参(6.13):决定【攒多久】【什么时候写】。 超线程(6.15):让一个核的两个线程填空闲周期。 六件事其实是一件事:把访问留在便宜的那一层。

四、三条能带走的

一、先量再优化。不知道瓶颈在哪一层的时候,任何优化都是猜。 二、改布局优先于改算法。同样的复杂度,常数能差十倍——因为布局决定了命中率。 三、把随机变顺序。排序、批量、合并——软件能做,而且不需要换硬件。

五、动手 Demo:总结——从寄存器到磁盘,一次访问多少钱

这一节把全表排出来,再把它换算成【假设 L1 = 1 秒】时人类能感知的时间,最后把整章的六类手段收成一张表。

rows = [("寄存器", "几百字节", 0.3), ("L1", "64 KB", 1.0),
        ("L2", "512 KB", 4.0), ("L3", "8 MB", 15.0),
        ("内存", "32 GB", 40.0), ("SSD", "1 TB", 40000.0),
        ("机械盘", "4 TB", 4000000.0)]
for name, cap, lat in rows:
    print(name, cap, lat, "纳秒")

# 假设 L1 命中 = 1 秒
for name, cap, lat in rows:
    print(name, "相当于", lat, "秒")

实跑输出:

--------------------------------------------------------------------
总结——从寄存器到磁盘,一次访问多少钱
--------------------------------------------------------------------
① 全表的量级(不是实测,是业界公认量级)

  这一层     典型容量     典型延迟           折成比例
  ------------------------------------------------------
  寄存器     几百字节     0.3 纳秒           0.3x
  L1         64 KB        1.0 纳秒           1.0x
  L2         512 KB       4.0 纳秒           4.0x
  L3         8 MB         15.0 纳秒          15.0x
  内存       32 GB        40.0 纳秒          40.0x
  SSD        1 TB         40000.0 纳秒       40000.0x
  机械盘     4 TB         4000000.0 纳秒     4000000.0x

  **注意最后两行的数量级**:**SSD 是内存的几千倍,机械盘是几百万倍。**
  **这不是【慢一点】,是【差一个世界】。**

② 换成人的时间尺度

  假设**一次 L1 命中 = 1 秒**,那么:

  这一层     相当于
  ---------------------
  寄存器     0.3 秒
  L1         1.0 秒
  L2         4.0 秒
  L3         15.0 秒
  内存       40.0 秒
  SSD        11.1 小时
  机械盘     46.3 天

  **内存要等 40 秒,SSD 要等 11 个小时,机械盘要等 46 天。**
  **所以程序里每多一次磁盘访问,代价就等于让你等半天。**

③ 于是整本书的这一章在说什么

  手段                         对付什么
  ----------------------------------------------------------
  缓存(6.1~6.6)              让重复访问落在快的那一层
  TLB(6.7)                   让地址翻译不用每次都查表
  页缓存(6.8)                让文件访问不必每次去磁盘
  缓冲与异步写(6.9~6.12)     把多次小访问攒成一次大访问
  调参(6.13)                 决定【攒多久】【什么时候写】
  超线程(6.15)               让一个核的两个线程填空闲周期

  **一句话总结整章**:**存储是分层的,每层差几十倍到几百万倍。**
  **所有性能优化的本质,都是把访问留在便宜的那一层。**

内存 40 秒、SSD 11.1 小时、机械盘 46.3 天。 这个换算不是玩文字游戏——它是【为什么优化要分层】的最好解释。在 40 秒这一层多花点心思,比在 46 天这一层省一点更划算吗?恰恰相反:46 天那一层随便省一次,抵得上 40 秒那一层省几千次。 所以优化的顺序是【从最慢的那一层开始】:先看有没有多余的磁盘 IO,再看内存访问,最后才看缓存行。 大多数人反过来做:一上来就调缓存行对齐,而真正的瓶颈是一次多余的 fsync。

📋 Linux 上看一次操作到底落在哪一层:perf stat -e cache-misses,minor-faults,major-faults——major-faults 是读盘的,最要命的那一个。

30 秒实验

  1. 把上表的数字全乘 2.5(换成更慢一代的机器),看比例变不变(比例基本不变,所以量级结论是稳的)。
  2. perf stat -e major-faults -r 3 python your.py,看有没有读盘的缺页。
  3. 给自己的一个脚本计时,估算其中磁盘 IO 占多少比例。

常见误区

  1. 【这些数字必须背精确值】。背量级就够了;不同代硬件差几倍很正常,差一个数量级才算记错。
  2. 【优化要先优化最快的层】。恰恰相反——最慢的那一层省一次,抵得上最快那层省几千次。
  3. 【缓存命中率上不去就没救了】。还有【批量】与【顺序化】两条路,往往比调缓存更有效。
自测三问(点开对答案) 1. 为什么说【每多一次磁盘访问,等于让人等半天】? —— **按 L1 = 1 秒的比例,SSD 是 11 小时、机械盘是 46 天。****所以一次多余的磁盘 IO 就是巨大的浪费。** 2. 优化的顺序应该是什么? —— **从最慢的那一层开始**:**先查多余的磁盘 IO,再看内存访问,最后才看缓存行。** 3. 整章的六类手段,共同点是什么? —— **都是【把访问留在便宜的那一层】**——**缓存、TLB、页缓存、批量、调参、超线程全是这个思路。**

相关笔记