128 MB 顺序读,块从 1 KB 到 8 MB:1 KB 要 718 毫秒(178 MB/秒),64 KB 只要 59 毫秒(2168 MB/秒)——差 12 倍。但从 64 KB 往上,再加块就不管用了,甚至略慢。两段成本交替主导,才有了【最优点】这种东西。
8.4 把测量器写好了,这一节用它做第一组正式实验:同样的字节数,只改每次读多少,看时间怎么变。
一、1 KB 与 64 KB 差 12 倍
Demo 82 读 128 MB,块从 1 KB 试到 8 MB。 1 KB:718 毫秒,178 MB/秒,调用 131072 次。 4 KB:258 毫秒,495 MB/秒,调用 32768 次。 64 KB:59 毫秒,2168 MB/秒,调用 2048 次。 从 1 KB 到 64 KB,调用次数除以 64,时间除以 12。这一段几乎是纯线性:每次调用的开销是固定的,调用少一次就省一次。 注意 178 MB/秒这个数——它比很多真硬盘的顺序读还慢。为什么会这样?因为这时候瓶颈根本不是硬盘,是【一秒钟只能做这么多次系统调用】。
二、再往上就不管用了
64 KB 之后,曲线平了,然后略微上翘。1 MB:114 毫秒,1121 MB/秒。8 MB:119 毫秒,1072 MB/秒。
为什么不再变快?因为固定成本已经摊薄到可以忽略:2048 次调用的开销是几十毫秒,128 次就只有一两毫秒。剩下的时间全花在【把数据从页缓存拷到你的缓冲区】上。
为什么反而变慢?因为每次 read 都要新分配一块 bytes 对象。8 MB 的分配 + 拷贝,成本比 64 KB 高得多,而且大块内存分配本身要跟操作系统打交道。
所以曲线是两条成本曲线的叠加:一条随块增大而下降(调用开销),一条随块增大而上升(拷贝与分配)。交点附近就是最优点。
三、两段成本模型
总时间 ≈ 调用次数 × 固定成本 + 总字节 ÷ 拷贝带宽。 前半段由固定成本主导:块小,调用多,时间随【1 ÷ 块大小】线性下降。 后半段由拷贝带宽主导:块大,调用少,时间趋于常数。 这个模型能解释三件事: ① 为什么小块慢:固定成本 × 巨大次数。 ② 为什么有个平台区:固定成本已经不重要了。 ③ 为什么太大又慢:模型里没写分配成本,实际里它会长起来。 工程上取 1 MB 左右是常见选择——它离平台区不远,又不会让内存压力太大。但严格说,最优点要自己量,它依赖机器与缓存状态。
四、真盘上这张表完全不同
沙箱里这些数字全是【内存带宽】,因为整份文件都在页缓存里。 真机械盘上,1 KB 与 8 MB 都能跑到接近 150 MB/秒——因为传输率是【设备给的】,块大小只影响一点点额外开销。在这个场景下,块大小的收益远没有沙箱里那么大。 而【真盘上与随机读的差距】才是数量级的:8.1 算过,接近 490 倍。所以【顺序读】这件事本身,比【块设多大】重要得多。 这也解释了为什么很多【调参优化】收益有限:它们在优化的那一项,本来就不是瓶颈。先看清瓶颈在哪一层,再决定调什么。
五、动手 Demo:顺序访问——为什么【块越大越快】会饱和
这次读 128 MB,块从 1 KB 一直试到 8 MB,把【调用次数】也一起打出来,看时间到底跟哪一个走。
import os, tempfile, time
KB, MB = 1024, 1024 * 1024
d = tempfile.mkdtemp(prefix="ch29c8_")
p = os.path.join(d, "big.bin")
size = 128 * MB
open(p, "wb").write(b"\0" * size)
def read_bs(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:
n += len(f.read(bs))
r.append((time.perf_counter() - t0) * 1000)
return min(r)
read_bs(MB, 1) # 预热
for bs in (KB, 4 * KB, 16 * KB, 64 * KB, MB, 8 * MB):
ms = read_bs(bs)
print("%5d KB 调用 %6d 次 %8.2f 毫秒 %6.0f MB/秒"
% (bs // KB, size // bs, ms, size / MB / (ms / 1000)))
实跑输出:
--------------------------------------------------------------------
顺序访问——为什么【块越大越快】会饱和
--------------------------------------------------------------------
① 128 MB 顺序读,块从 1 KB 到 8 MB
每次读 调用次数 耗时 带宽
------------------------------------------------------
1 KB 131072 718.01 毫秒 178 MB/秒
4 KB 32768 258.40 毫秒 495 MB/秒
16 KB 8192 94.06 毫秒 1361 MB/秒
64 KB 2048 59.04 毫秒 2168 MB/秒
1 MB 128 114.17 毫秒 1121 MB/秒
8 MB 16 119.35 毫秒 1072 MB/秒
**1 KB 是最慢的,64 KB 是最快的,差 12.2 倍。**
**但从最快那一档往上,再加块就不管用了,甚至略慢。**
**说明瓶颈已经从【调用次数】换成了【拷内存的速度】。**
② 为什么会饱和
**每次 `read` 有两段成本**:
**一段是固定的(进内核、查页、返回),一段是随字节数长的(拷到用户缓冲区)。**
**调用次数多的时候,第一段是瓶颈,所以块越大越快、并且接近线性。**
**块大到一定程度,第一段摊薄了,第二段成了瓶颈,再加块也没用。**
**模型**:**总时间 ≈ 调用次数 × 固定成本 + 总字节 ÷ 拷贝带宽。**
**前半段由固定成本主导,后半段由拷贝带宽主导。**
**所以【块大小】有一个最优区间**:**太小是调用开销压死,太大是内存占用与首字节延迟变差。**
**1 MB 左右是常见选择。**
③ 在真机械盘上,这张表会长得很不一样
**沙箱里这些数字全是【内存带宽】**,**因为整份文件都在页缓存里。**
**真机械盘上,1 KB 与 8 MB 都能跑到接近 150 MB/秒**,
**因为传输率是设备给的,块大小只影响一点点额外开销。**
**而【真盘上与随机读的差距】才是数量级的**(8.1 算过,接近 490 倍)。
📋 **Linux 上对比**:`dd if=大文件 of=/dev/null bs=4k` 与 `bs=1M` 各跑一次;
**再 `echo 3 | sudo tee /proc/sys/vm/drop_caches` 清缓存后重跑**,**两组数会完全不同。**
看【调用次数】那一列:从 131072 掉到 16,降了 8000 倍;而时间只降了 6 倍。说明调用开销只是成本的一部分。
1 KB 那档的 178 MB/秒不是硬盘速度,是【系统调用速度】。这是最容易读错的地方。
64 KB 附近是最好的区间:调用次数已经不多,单次分配还不算大。
1 MB 之后反而慢一点,因为每次 read 都要新分配一块大内存并拷进去。
📋 Linux 上对比:dd if=大文件 of=/dev/null bs=4k 与 bs=1M 各跑一次;
📋 再 echo 3 | sudo tee /proc/sys/vm/drop_caches 清缓存后重跑,两组数会完全不同。
30 秒实验
- 把 8 MB 那档去掉,加上 128 B,看会不会更慢。
python -c "import os;print(os.cpu_count())",想一想这和块大小有没有关系。- 改用
f.readinto(bytearray)代替f.read(),看大块那档能不能救回来。
常见误区
- 【1 KB 那档 178 MB/秒说明这台机器内存很慢】。它说明的是【系统调用很慢】,数据搬运速度远高于此(64 KB 那档 2168 MB/秒才是)。
- 【最优点是一个固定值,记下来就行】。最优点随机器、缓存状态、是否直读而变,要用自己的场景量一遍。
- 【块大小调到最优,程序就快了】。如果真正的瓶颈是磁盘随机寻道(8.6),调块大小收益极其有限。
自测三问(点开对答案)
1. 为什么 1 KB 那档只有 178 MB/秒,而 64 KB 能到 2168 MB/秒? —— **因为 1 KB 那档的瓶颈是【系统调用次数】**:**131072 次调用的固定开销加起来,比搬数据的时间还长。****64 KB 那档调用只有 2048 次,开销被摊薄了。** 2. 为什么块大到 1 MB 之后反而变慢? —— **因为每次 `read` 都要新分配一块 bytes 并拷贝**,**块越大,这一笔分配与拷贝越贵,****大到一定程度就压过了省下的调用开销。** 3. 在真机械盘上,块大小的收益为什么远没有沙箱里大? —— **因为真盘的顺序读速度由【设备传输率】决定(约 150 MB/秒)**,**块大小只影响一点点额外开销。****沙箱里快的那些倍率,来自【省掉系统调用】,不是【硬盘变快】。**- ☐ 8.6 随机访问
相关笔记
- 实验程序 — 这个测量器的写法
- 随机访问 — 把走法换成随机
- 页面缓存 — 为什么整份文件都在内存里
- 🐧 Linux是怎样工作的 · 索引