🐧 第八章 外部存储器
📄 一句话概括

做实验之前先量【尺子】:这台机器上 open + close 一次 60 微秒,seek + read 4 KB 一次 6.69 微秒——而真机械盘同样一次要 12690 微秒,差 1898 倍。所以在这个沙箱里,随机读磁盘这件事根本不存在。

📝 本节和前面几节的关系

8.1 与 8.2 全是【模型推算】,这一节开始做实验——但第一步不是测硬盘,是先测【测量工具自己有多准】。

一、先量尺子,再量东西

如果一把尺子的每一格都歪 5 毫米,你量什么都是错的。软件测量也是这样:每次操作都有一笔固定开销(进内核、函数调用、计时器本身)。 这笔开销如果和目标值同量级,实验就失去意义。 所以第一步永远是:空跑一遍,看【什么都不做】要多久。 Demo 80 量了三样东西:纯 Python 空循环、open + close、seek + read 4 KB。

二、这台机器量出来多少

空循环 2000 次 0.08 毫秒,一次 0.04 微秒——这一笔可以忽略。 open + close 一次 60.92 微秒。这个数很值得记住:光是开一个文件句柄,就比一次内存访问贵几百倍。 seek + read 4 KB 一次 6.69 微秒。比 open 还便宜,因为不涉及句柄创建,就是移动一个内存里的游标、再从页缓存拷 4 KB 出来。 这三个数就是这台机器的【量程下限】:任何小于 10 微秒的差别,在这里都测不准。

三、差了 1898 倍说明什么

真机械盘上一次 seek + read 4 KB 是 12690 微秒。这台机器上是 6.69 微秒。差 1898 倍。 差这么多,不是【机器快】,是【做的事不一样】。真盘上:磁头要真的移过去,盘子要真的转过来。沙箱里:文件刚写完就在页缓存里,seek 只是改一个整数。 所以【在这台机器上测随机访问】测得再好,结论也不能用来描述机械盘。这不是实验没做好,是实验对象不存在。 这句话很重要:很多【实测数据】的错,不是因为测错,而是因为测的不是那个东西。比如拿内存盘测数据库性能,拿空表测查询优化。

四、测不到的时候怎么写结论

三条诚实的做法,按可信度从高到低。 ① 量清尺子的下限,明确说【小于这个数的差别测不出】。这是本节做的。 ② 用模型算,并标清这是模型。8.1 与 8.2 的表全是模型——参数来自真盘规格书,算出来的倍数关系是可信的,绝对值要看具体盘。 ③ 真要实测,就换机器并绕过缓存。用 iflag=direct 或 --direct=1,并且一定在【有目标介质】的机器上跑。 最不诚实的是第四种:在缓存里测出个数字,然后说【实测硬盘随机读是 6 微秒】。这个错误在技术文章里非常常见。

五、动手 Demo:HDD 的实验——先量清尺子本身

分三次各跑 2000 遍:什么都不做的空循环、open + close、seek + read 4 KB, 看这台机器的固定开销在什么量级。

import os, tempfile, time

d = tempfile.mkdtemp(prefix="ch29c8_")
p = os.path.join(d, "probe.bin")
open(p, "wb").write(b"\0" * (4 * 1024 * 1024))

def t(f, n=2000):
    t0 = time.perf_counter()
    f(n)
    return (time.perf_counter() - t0) * 1000

def loop_only(n):
    x = 0
    for _ in range(n):
        x += 1
    return x

def open_close(n):
    for _ in range(n):
        open(p, "rb").close()

def seek_read(n):
    f = open(p, "rb", buffering=0)
    for i in range(n):
        f.seek(i * 2048)
        f.read(4096)
    f.close()

for name, fn in (("空循环", loop_only),
                 ("open + close", open_close),
                 ("seek + read 4 KB", seek_read)):
    ms = min(t(fn) for _ in range(3))
    print("%-18s %7.2f 毫秒  每次 %6.2f 微秒" % (name, ms, ms * 1000 / 2000))

实跑输出:

--------------------------------------------------------------------
HDD 的实验——先量清尺子本身
--------------------------------------------------------------------
① 做 IO 实验之前,先知道测量工具的下限

  **如果【一次空操作】就要 50 微秒,那你就测不出 30 微秒的差别。****所以先量这几笔固定开销。**

  操作                 2000 次耗时     每次折算
  ---------------------------------------------------
  空循环               0.08 毫秒       0.04 微秒
  open + close         121.85 毫秒     60.92 微秒
  seek + read 4 KB     13.37 毫秒      6.69 微秒

  **一次 seek + read 4 KB 只要 7 微秒。**
  **而 8.1 算过:真机械盘上同样一次要 12690 微秒。**
  **差 1898 倍。**

② 这个差距说明了什么

  **说明这个沙箱里的【随机读】根本不是【随机读磁盘】。**
  **文件刚写完就在页缓存里,`seek` 只是移动一个内存里的游标。**
  **所以在这台机器上,无论怎么改访问顺序,都测不出 HDD 的特性。**

  **这不是【实验没做好】,是【实验对象不存在】。****诚实的做法是:**
  **① 量清尺子的下限(就是这一节做的);**
  **② 用【模型】算真盘上的数字(8.1、8.2 已经算了);**
  **③ 真要实测,就把机器换成有机械盘的,并绕过缓存。**

  📋 **绕过缓存去测真盘**:`sudo dd if=/dev/sda of=/dev/null bs=4k count=10000 iflag=direct`;
  **对比不加 `iflag=direct` 的那一次**,差多少就是缓存的作用。

这三个数是【尺子的刻度】,不是【硬盘的性能】。 open + close 一次 60 微米左右——这是 Windows 上创建句柄的代价,在 Linux 上会低得多。 seek + read 一次 6 到 7 微秒,比 open 还便宜,因为不建句柄。 真盘上一次 12690 微秒,比这里贵近两千倍。这个差距说明:这台机器上根本没有【磁盘随机读】这回事。

📋 绕过缓存去测真盘:sudo dd if=/dev/sda of=/dev/null bs=4k count=10000 iflag=direct; 📋 对比不加 iflag=direct 的那一次,差多少就是缓存的作用。

30 秒实验

  1. 改一下:把 open 提到循环外面,只跑 seek + read,看差多少。
  2. time dd if=/dev/zero of=/tmp/t.bin bs=1M count=200,看写盘的实际速度。
  3. 想一想:为什么计时器本身也有成本?它测 1 微秒的东西准不准?

常见误区

  1. 【实测最可靠,模型都是纸上谈兵】。测错对象的【实测】比老实交代的模型更糟——它给人一个假的确定感。
  2. 【跑一次就够了,测得多了没意义】。单次结果里混着系统噪声(后台回写、GC、其他进程),要重复取最小值。
  3. 【Windows 上测得慢,说明 Windows 的 IO 比 Linux 差】。open 慢是句柄机制的差别,和磁盘读写速度是两件事。
自测三问(点开对答案) 1. 为什么做 IO 实验之前要先量空操作的时间? —— **因为每次测量都有固定开销**,**如果这笔开销和目标值同量级,结果就被它淹没了。****先量出【量程下限】,才知道哪些结论不能下。** 2. 沙箱里 `seek` + `read` 4 KB 是 6.69 微秒,真盘是 12690 微秒,为什么差这么多? —— **因为两件完全不同的事**:**真盘要移动机械臂、等盘片转**;**沙箱里文件在页缓存,`seek` 只是改一个整数。****所以这个差距不是【快】,是【测的不是同一个东西】。** 3. 测不到的时候,有哪三种诚实的写法? —— **① 量清尺子下限,明说测不出什么;② 用模型算,并标明是模型;③ 换机器、绕过缓存,做真实测。****最不该做的是把缓存内的数字说成硬盘的数字。**

相关笔记