一个能重复用的 IO 测量器要做三件事:造数据、预热、重复取最小。64 MB 顺序读,块从 4 KB 加到 256 KB 快了 5.6 倍,再往上反而慢——因为成本从【调用次数】换成了【拷内存】。
8.3 量好了尺子,这一节把测量器写出来:一个能反复用的最小实验程序,后面 8.5、8.6 的结论都从它来。
一、测量器的三件事
① 造数据:先写一个固定大小的文件,别每次重新生成。否则你测的是【生成数据】而不是【读数据】。
② 预热:先完整读一遍,把页缓存填上。这一条决定了你测的是【盘】还是【缓存】。想测盘就要反着做:清缓存(drop_caches)或用直读。
③ 重复取最小:跑三到五次,取最小值。为什么取最小不取平均?因为噪声只会让时间变长,不会让时间变短——最小值最接近【没有干扰时的真实值】。
这三条是所有性能实验的通用套路,换成 CPU、网络、数据库都一样。
二、真跑:只改每次读多少
Demo 81 读 64 MB,块从 4 KB 试到 4 MB。
4 KB:108.63 毫秒(589 MB/秒);256 KB:19.23 毫秒(3328 MB/秒)。快了 5.6 倍。
这一段的收益全部来自【少做了一万多次系统调用】:64 MB ÷ 4 KB = 16384 次,64 MB ÷ 256 KB = 256 次。
但 1 MB 之后又慢回去了:1 MB 46.19 毫秒,4 MB 45.31 毫秒,都比 256 KB 慢。因为大块 read 每次都要新分配一块内存、再拷进去,块越大这笔拷贝越贵。
两笔成本一涨一落,中间就有个最优点。在页缓存命中的情况下,这个点落在几十 KB 到 256 KB 之间。
三、三个坑
① 没预热就测:第一次读到的是【冷】的那一侧,数字会大一圈;预热之后测得的是【热】的那一侧,只能代表缓存。两边都不是【盘的真实速度】。 ② 只跑一次:一次可能正好赶上后台回写、GC、或者别的进程抢 IO。多跑几次取最小值。 ③ 边写边读:回写线程会和读抢设备,所以读写要分开测,中间留点间隔。这一点在 SSD 上尤其明显(写会触发垃圾回收)。 这三条在 4.13 讲过一遍,IO 实验的噪声比 CPU 实验大一个量级,所以这里再强调一次。
四、换更专业的工具
自己写的测量器适合【理解原理】,真做工程要用 fio。
fio 能控的东西:块大小、顺序还是随机、队列深度、几线程、直读还是过缓存、跑多久。
一条标准写法:fio --name=t --rw=read --bs=4k --size=1G --direct=1。--direct=1 是绕过缓存——不加它,你测的就是内存。
iostat -x 1 看实时指标:r/s、w/s 是每秒请求数,rkB/s 是带宽,aqu-sz 是平均队列长度。队列长度长期大于 1,说明设备是瓶颈。
五、动手 Demo:实验程序——一个能重复用的顺序读测量器
造一个 64 MB 的文件,预热一遍,然后只改每次 read 的字节数,跑五档,每档取三次里的最小值。
import os, tempfile, time
KB, MB = 1024, 1024 * 1024
d = tempfile.mkdtemp(prefix="ch29c8_")
p = os.path.join(d, "seq.bin")
size = 64 * MB
open(p, "wb").write(b"\0" * size)
def read_with(bs, reps=3):
r = []
for _ in range(reps):
t0 = time.perf_counter()
with open(p, "rb", buffering=0) as f:
n = 0
while n < size:
b = f.read(bs)
n += len(b)
r.append((time.perf_counter() - t0) * 1000)
return min(r)
read_with(MB, 1) # 预热
for bs in (4 * KB, 64 * KB, 256 * KB, MB, 4 * MB):
ms = read_with(bs)
print("%4d KB %8.2f 毫秒 %6.0f MB/秒"
% (bs // KB, ms, size / MB / (ms / 1000)))
实跑输出:
--------------------------------------------------------------------
实验程序——一个能重复用的顺序读测量器
--------------------------------------------------------------------
① 测量器的三件事
**① 造数据**:**写一个固定大小的文件,别每次重新生成。**
**② 预热**:**先读一遍,让它在缓存里(或先清缓存)。**
**这一条决定了你测的是【盘】还是【缓存】。**
**③ 重复取最小**:**取最小值压掉系统噪声,而不是取平均。**
② 真跑:64 MB 顺序读,只改每次读多少
每次读 读 64 MB 耗时 折算带宽
-------------------------------------------
4 KB 108.63 毫秒 589 MB/秒
64 KB 24.39 毫秒 2624 MB/秒
256 KB 19.23 毫秒 3328 MB/秒
1 MB 46.19 毫秒 1386 MB/秒
4 MB 45.31 毫秒 1412 MB/秒
**从 4 KB 到 256 KB,快了 5.6 倍。**
**再往上就不快了,反而慢一点。**
**因为大块 `read` 每次都要新分配一块内存、再把数据拷进去**,
**块越大,这一笔拷贝越贵。**
**两笔成本一涨一落,所以中间有个最优点。**
③ 这个测量器要注意的三个坑
**① 没预热就测**:**第一次读到的是【冷】的那一侧**,**数字会大一圈。**
**预热之后测的是【热】的那一侧,只能代表缓存。**
**② 只跑一次**:**一次可能正好赶上后台回写或者 GC。****要多跑几次取最小值。**
**③ 边写边读**:**回写线程会和读抢设备**,**所以读写要分开测,中间留一点间隔。**
**这三条在 4.13 讲过一遍**,**这里再强调一次是因为 IO 实验的噪声比 CPU 实验大得多。**
📋 **Linux 上更专业的工具**:`fio`(能控队列深度、块大小、顺序/随机、直读);
**`fio --name=t --rw=read --bs=4k --size=1G --direct=1` 是标准写法。**
先看 4 KB 那一行:589 MB/秒。这个速度已经接近内存带宽了——说明整份文件都在页缓存里,一次设备读都没发生。
从 4 KB 到 256 KB 快 5.6 倍,因为系统调用从 16384 次降到 256 次。
1 MB 之后反而慢:大块 read 要新分配内存再拷贝,这笔拷贝成本压过了省下的调用开销。
所以【块越大越好】只在【调用开销是瓶颈】的区间成立。这个区间在哪里,取决于你的机器和缓存状态。
📋 Linux 上更专业的工具:fio(能控队列深度、块大小、顺序/随机、直读);
📋 fio --name=t --rw=read --bs=4k --size=1G --direct=1 是标准写法。
30 秒实验
- 把
buffering=0去掉,看 4 KB 那一档快多少。 - 把文件加大到 512 MB(比内存小一点),看 4 KB 那档怎么变。
iostat -x 1,一边跑这个 Demo 一边看aqu-sz。
常见误区
- 【块越大越快,所以永远用最大的块】。大块要占内存、要拷一份、首字节延迟也变长,过了最优点就只是更慢。
- 【取最小值是作弊,应该取平均】。噪声只会加时间不会减时间,最小值最接近无干扰的真实值,平均值反而被偶发的卡顿污染。
- 【预热只是为了快一点,可有可无】。预热决定了你测的是【缓存】还是【盘】,这是【测什么】的问题,不是【快慢】的问题。
自测三问(点开对答案)
1. 为什么重复测要取最小值而不是平均? —— **因为干扰只会让耗时变长**,**最小值最接近【没有干扰时的真实值】。****平均值会被偶发的卡顿拉高,反映的是一个不存在的【典型情况】。** 2. 64 MB 顺序读,块从 4 KB 加到 256 KB 为什么快了 5.6 倍? —— **因为系统调用从 16384 次降到 256 次**,**每次调用的固定开销被摊薄了。****这一段的速度完全由【调用次数 × 单次开销】决定。** 3. 为什么 1 MB 之后反而变慢? —— **因为成本换了主角**:**调用开销已经摊薄到可忽略,剩下的是【每次分配一大块内存再拷进去】的成本,****块越大这一笔越贵。**- ☐ 8.5 顺序访问
相关笔记
- HDD的实验 — 尺子的刻度
- 顺序访问 — 用这个测量器做的第一组实验
- 实验方法 — 同样的三个坑
- 🐧 Linux是怎样工作的 · 索引