Python 的 open() 自带一层缓冲:不带缓冲读 64 MB 要 105 毫秒,带默认缓冲 66 毫秒,缓冲设成 1 MB 只要 26 毫秒。但这只是一层【用户态】缓冲——内核的预读、块层的合并、设备的寻道,在沙箱里一层也测不到。
8.9 讲了预读的原理,这一节做实验——但实验的结果是【能测到哪一层】,而不是【预读快多少】。知道边界,比多一个数字重要。
一、Python 的缓冲本身就是一层
open(p, "rb") 默认带缓冲(io.BufferedReader,默认 8 KB 左右,可调)。你 read(4096) 一次,它可能只从内核拿了 8 KB 出来,剩下的等你下次要。
open(p, "rb", buffering=0) 完全不带缓冲,每次 read(n) 就是一次货真价实的系统调用。
实测(读 64 MB,每次 4 KB):带默认缓冲 66.27 毫秒(966 MB/秒);不带缓冲 105.02 毫秒(609 MB/秒);缓冲设成 1 MB 只有 26.65 毫秒(2401 MB/秒)。
不带缓冲慢 1.6 倍——因为一万六千次 read(4096) 就是一万六千次系统调用;带缓冲时,只在缓冲块用完时才真的调一次。
缓冲设成 1 MB 最快——因为系统调用次数又降了一个量级。这跟 8.5 的结论是一致的:快了不是因为盘快,是因为【少调了几次】。
二、这一层是用户态的,不是内核的
io.BufferedReader 是 Python 运行时里的东西。它和内核的页缓存、预读窗口完全是两回事。所以这里测出来的差别是【用户态缓冲】的差别。
内核预读那一层,在沙箱里测不出来——因为文件本来就在页缓存里,预读没什么可预的。要测它,得有【真正的缺页】:清缓存(drop_caches)或者用 O_DIRECT 绕过缓存。
区分这两层非常重要:很多【我的程序比你的快】的比较,差的其实就在用户态缓冲这一层,跟磁盘、跟内核一点关系都没有。
三、沙箱里到底能测到哪一层
能测:应用缓冲(本节)、系统调用开销、页缓存命中。这三层都在【内存里】,沙箱里跑的本来就是它们。 测不准:块层的合并与调度。因为页缓存挡在前面,请求很可能根本没走到那一层。 不能测:设备的寻道与旋转。没有机械盘,也没有绕过缓存的手段。 这张表比任何数字都有用:它告诉你【哪一条结论有实测支撑、哪一条只是模型推算】。写实验报告时把这两类分开,才是诚实的做法。 这也是这一整章的方法论总结:先量尺子(8.3)、再设计实验(8.4)、看清能测到哪一层(本节)、最后才下结论。
四、动手 Demo:实验——在沙箱里能测到哪一层
同一个 64 MB 文件,每次读 4 KB,分三种读法:带默认缓冲、不带缓冲、缓冲设成 1 MB,看用户态缓冲这一层能差多少。
import os, tempfile, time
KB, MB = 1024, 1024 * 1024
d = tempfile.mkdtemp(prefix="ch29c8_")
p = os.path.join(d, "buf.bin")
size = 64 * MB
open(p, "wb").write(b"\0" * size)
def read_with(**kw):
r = []
for _ in range(3):
t0 = time.perf_counter()
with open(p, "rb", **kw) as f:
n = 0
while n < size:
b = f.read(4096)
if not b:
break
n += len(b)
r.append((time.perf_counter() - t0) * 1000)
return min(r)
cases = (("带默认缓冲", {}),
("不带缓冲", {"buffering": 0}),
("缓冲 1 MB", {"buffering": MB}))
for name, kw in cases:
ms = read_with(**kw)
print("%-12s %8.2f 毫秒 %6.0f MB/秒" % (name, ms, 64 / (ms / 1000)))
实跑输出:
--------------------------------------------------------------------
实验——在沙箱里能测到哪一层
--------------------------------------------------------------------
① Python 的缓冲本身就是一层预读
**`open(p, "rb")` 默认带缓冲**(`io.BufferedReader`,**默认 8 KB 左右,可调**)。
**`open(p, "rb", buffering=0)` 完全不带缓冲**,每次 `read(n)` 就是一次系统调用。
读法 64 MB 耗时 折算带宽
------------------------------------------------
带默认缓冲 90.90 毫秒 704 MB/秒
不带缓冲 130.10 毫秒 492 MB/秒
缓冲设成 1 MB 47.56 毫秒 1346 MB/秒
**不带缓冲比带缓冲慢 1.4 倍。**
**因为不带缓冲时,一万六千次 `read(4096)` 就是一万六千次系统调用;**
**带缓冲时,它只在缓冲块用完时才真的调一次。**
② 这一层是【用户态的预读】,不是内核的
**`io.BufferedReader` 属于 Python 运行时,它和内核的页缓存、预读窗口是两回事。**
**所以这里测出来的差别是【用户态缓冲】的差别。**
**内核预读那一层,在沙箱里测不出来**——**因为文件本来就在页缓存里,预读没什么可预的。**
③ 在沙箱里到底能测到哪一层
层次 沙箱能测吗 为什么
----------------------------------------------------------------------
应用缓冲(本节) 能 纯用户态,与介质无关
系统调用开销 能 进内核的那一趟是真的
页缓存命中 能 它本来就在走这条路
块层合并与调度 测不准 页缓存挡在前面,请求没到那一层
设备寻道与旋转 不能 没有机械盘,也没有绕过缓存
**这张表比数字有用**:**它告诉你【哪一条结论有实测支撑、哪一条只是模型推算】。**
**写实验报告时,把这两类分开,才是诚实的做法。**
📋 **要测到最底下那一层**:`dd iflag=direct`(绕过缓存)+ `fio --direct=1`;
**并且一定要在有【目标介质】的机器上测**——拿内存盘测机械盘的特性,测不出来。
三种读法读的是同一个文件、同样的字节数,唯一差别是用户态缓冲多大。 带缓冲比不带缓冲快 1.6 倍左右,因为系统调用从一万六千次降到两千次。 缓冲设成 1 MB 最快:系统调用又降一个量级,而且 1 MB 的拷贝效率比 8 KB 高。 但请注意:这三个数字都是【内存速度】,没有一个是磁盘速度。在这台机器上,你测不出硬盘。
📋 要测到最底下那一层:dd iflag=direct(绕过缓存)+ fio --direct=1;
📋 并且一定要在【有目标介质】的机器上测——拿内存盘测机械盘的特性,测不出来。
30 秒实验
- 把
read(4096)改成read(1),看三者差距会不会拉得更大。 - 用
f.readinto(bytearray(4096))代替f.read(4096),看能省多少。 grep . /proc/sys/vm/vfs_cache_pressure,看看你的系统缓存压力参数。
常见误区
- 【不开缓冲一定更快,缓冲是多一层拷贝】。缓冲省下的系统调用远比那一次拷贝值钱,所以带缓冲快 1.6 倍。
- 【Python 的缓冲就是操作系统的预读】。Python 的缓冲在用户态,预读在内核态,两者是独立的两层。
- 【只要代码写得对,在哪儿测都一样】。测试环境决定了你能测到哪一层。没有机械盘的机器,永远测不出寻道。
自测三问(点开对答案)
1. 为什么不带缓冲反而慢? —— **因为每次 `read(4096)` 都真的做一次系统调用**,**一万六千次的调用开销加起来,比带缓冲多做一次拷贝贵得多。****带缓冲只在缓冲块用完时才调一次。** 2. Python 的缓冲和内核的预读有什么区别? —— **Python 的缓冲在用户态(`io.BufferedReader`),它把系统调用次数降下来;****内核的预读在页缓存层,它把【缺页】提前补上。****两层独立,可以同时存在。** 3. 在沙箱里为什么测不到块层与设备层? —— **因为页缓存挡在前面**:**请求根本走不到块层,更到不了设备。****要测下层,必须绕过缓存(O_DIRECT)并换到有目标介质的机器上。**- ☐ 8.11 SSD
相关笔记
- HDD的实验 — 量尺子的那一节
- 预读 — 被这一节证明测不出来的东西
- 读取文件的实验 — 第 6 章同一类实验
- 🐧 Linux是怎样工作的 · 索引