做实验之前先量【尺子】:这台机器上 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 秒实验
- 改一下:把
open提到循环外面,只跑seek+read,看差多少。 time dd if=/dev/zero of=/tmp/t.bin bs=1M count=200,看写盘的实际速度。- 想一想:为什么计时器本身也有成本?它测 1 微秒的东西准不准?
常见误区
- 【实测最可靠,模型都是纸上谈兵】。测错对象的【实测】比老实交代的模型更糟——它给人一个假的确定感。
- 【跑一次就够了,测得多了没意义】。单次结果里混着系统噪声(后台回写、GC、其他进程),要重复取最小值。
- 【Windows 上测得慢,说明 Windows 的 IO 比 Linux 差】。
open慢是句柄机制的差别,和磁盘读写速度是两件事。
自测三问(点开对答案)
1. 为什么做 IO 实验之前要先量空操作的时间? —— **因为每次测量都有固定开销**,**如果这笔开销和目标值同量级,结果就被它淹没了。****先量出【量程下限】,才知道哪些结论不能下。** 2. 沙箱里 `seek` + `read` 4 KB 是 6.69 微秒,真盘是 12690 微秒,为什么差这么多? —— **因为两件完全不同的事**:**真盘要移动机械臂、等盘片转**;**沙箱里文件在页缓存,`seek` 只是改一个整数。****所以这个差距不是【快】,是【测的不是同一个东西】。** 3. 测不到的时候,有哪三种诚实的写法? —— **① 量清尺子下限,明说测不出什么;② 用模型算,并标明是模型;③ 换机器、绕过缓存,做真实测。****最不该做的是把缓存内的数字说成硬盘的数字。**- ☐ 8.4 实验程序
相关笔记
- HDD的数据读写机制 — 被参照的那套参数
- 实验 — 沙箱到底能测到哪一层
- 实验方法 — 同样的【先量尺子】
- 🐧 Linux是怎样工作的 · 索引