骗局的价目表
这本书从头到尾在说一件事:你以为你独占整台机器,而维持这个假象是要花钱的。每一章都往账单上添了一行。现在,把它们放在一起。
整张表
点每一行看它是什么、来自哪一章。把「做多少次」的滑块拖到你的实际量级,看总账。
注意条形是对数刻度——不这么画的话,前十行会挤成一条看不见的线。
先感受一下这个跨度
表格最上面那行是 1 ns,最下面那行是 8 ms。比例是八百万倍。
这个数字大到失去直觉了,所以换个说法。把 1 纳秒当成 1 秒,整张表变成:
| 操作 | 真实耗时 | 如果 1 ns = 1 秒 |
|---|---|---|
| L1 缓存命中 | 1 ns | 1 秒 |
| 主存访问 | 80 ns | 1 分半 |
| 一次系统调用 | 100 ns | 2 分钟 |
| 走一趟四级页表 | 120 ns | 2 分钟 |
| 进程上下文切换 | 3 μs | 50 分钟 |
| NVMe 随机读 4 KB | 100 μs | 1 天多 |
| 同机房一个来回 | 500 μs | 6 天 |
| 一次 fsync | 1 ms | 11 天 |
| 一次主缺页 | 3 ms | 1 个多月 |
| 机械盘一次寻道 | 8 ms | ★ 3 个月 |
一秒钟,和三个月。这就是你每天在写的代码里,两条相邻语句之间可能存在的差距。
这张表最重要的用法
看见任何一段代码,先问:它在这张表的第几行?
然后你会立刻发现,绝大多数「性能优化」是在错误的行上使劲。
举个具体的:你花了一下午把一个跑一万次的循环优化了 30%,省下 3 μs。同一个函数里有一次 fsync,值 1000 μs。
你的一下午,对总耗时的贡献是 0.3%。
而如果你把那次 fsync 和另外 99 次合并成一次批量提交(第 18 章那个组提交),你能省下 99%。五分钟的改动,三个数量级的收益。
这不是说微观优化没意义——在已经确认热点在那一行时它很有意义。问题在于「确认」这一步经常被跳过,而人的直觉在跨七个数量级的问题上完全不可靠。
三条从表里直接读出来的经验
1. 批量化:把固定成本摊薄
表里很多操作有固定成本——不管你办多少事,进门那一趟的钱是一样的。系统调用 100 ns,你读 1 字节和读 128 KB 都要付。
于是「攒一批一次办」这个模式在各处反复出现,而且现在你能看出它们是同一件事:
| 你见过的做法 | 摊薄的是哪一行 | 本书哪章 |
|---|---|---|
stdio 的缓冲区 / BufferedInputStream | 系统调用 | 第 3 章 |
| 数据库的组提交 | fsync | 第 18 章 |
| 网络的 Nagle 算法、批量发包 | 网络往返 | 第 20 章 |
| RecyclerView 的批量 notify、Compose 的重组批处理 | 上下文切换 + 渲染 | 第 9 章 |
Flow 的 buffer/conflate、日志的异步落盘 | 系统调用 + 切换 | 第 3、9 章 |
它们看起来是五个不同领域的技巧,其实是同一条原则的五次应用。
2. 局部性:让你留在表的上半部分
表的前几行(缓存、TLB)和后面差着几个数量级,而你能不能留在前几行,几乎完全取决于访问模式。
顺序访问一个数组:缓存预取生效,一个 TLB 条目管 1024 个 int。你活在第 1 行。
随机访问同样大的数组:几乎每次都 cache miss,而且很可能 TLB miss。你掉到了第 3–5 行,慢两百倍。
数据结构的选择、内存布局、遍历顺序——这些「看起来很基础」的东西之所以影响巨大,就是因为它们决定了你在这张表的哪一段。这也是为什么面向数据的设计(数组的 struct 而不是 struct 的数组)在性能敏感场合效果显著。
3. 异步化:不是让它变快,是别让人干等
表下半部分那些毫秒级的操作,你没法让它们变快——磁盘就那么快,网络就那么远。
能做的是:等待的时候别占着 CPU。这就是异步 IO、epoll、协程存在的全部理由(第 19 章)。
注意这个区别很关键:异步不会让单次操作变快,它让你在等待期间能干别的。如果你的负载是纯 CPU 密集的,异步化一点用都没有,甚至因为多了调度开销而更慢。
关于这些数字的诚实说明
这张表是现代 x86-64 Linux 上的典型量级。换台机器、换个内核版本、换块盘,每一行都会浮动,有些能浮动好几倍:
- 系统调用在 KPTI 之后明显变贵(第 3 章),而有没有 PCID 又影响很大。
- NVMe 的延迟,消费级和企业级能差三倍。
- fsync 极度依赖硬件——有没有带电池的写缓存,能差一个数量级。
- 上下文切换的间接成本(第 9 章)完全取决于你的工作集大小。
但它们之间的比例关系是稳的,而那才是你需要记住的东西。
要拿到你自己机器上的真实数字,可以用:perf bench(系统调用、内存、调度)、fio(磁盘)、lmbench(综合)、sysbench。量一遍你自己的机器,比背这张表有用得多。
回到开头那个谎言
第 1 章说:你的程序有一个从没被质疑过的信念——这台机器是我的。
现在你知道了这个信念的全部内幕:
- 你以为你独占 CPU——其实你每秒被抢走上千次控制权,靠一个硬件时钟信号维持秩序,而调度器用一只按权重走快慢的虚拟表决定谁下一个上(卷 II)。
- 你以为你独占内存——其实你写的每个地址都要被切成
9|9|9|9|12走完四级页表,而「分配成功」只是一句承诺,兑现发生在你第一次动手写的时候(卷 III)。 - 你以为你在读一个文件——其实你只拿到了一个数组下标,而文件本身连名字都没有(卷 IV)。
撑着这三重假象的,是硬件上那两个比特(卷 I)。而每一次你跨过那道由两个比特划出的线,都要付一次钱——那些钱,就是这张表。
最后你还亲手把整个骗局又演了一遍,那就是容器(卷 V)。
接下来读什么
这本书的目标是画一张地图,不是穷尽细节。如果你想继续往下走:
| 你想 | 去读 | 为什么是它 |
|---|---|---|
| 系统地重学一遍,带练习 | ★ 《Operating Systems: Three Easy Pieces》 | 免费在线,三大主题正好对应虚拟化/并发/持久化。写得极其好读,是这本书最自然的下一站 |
| 理解机器本身,而不只是 OS | CSAPP(深入理解计算机系统) | 从二进制表示一路讲到链接、异常控制流、并发。第 9 章那本书讲得比这里深得多 |
| 亲手写一个内核 | xv6 | MIT 的教学 Unix,几千行 C,配套课程和实验都公开 |
| 深入 Linux 具体实现 | 《Linux 内核设计与实现》/内核文档 | 这本书讲机制,那些讲 Linux 是怎么实现这些机制的 |
| 性能分析实战 | Brendan Gregg 的《Systems Performance》 | 第 23 章那套排障方法的完整版,工具和方法论都极其系统 |
- 《同时》——这本书讲机器提供了什么,那本讲你该怎么用:协程、事件循环、锁、内存可见性。第 8、9、19 章都在那边有对应的另一侧。
- 《开门》——第 2 章的 ring 边界和第 22 章的容器,是「怎么给不受信任的代码划边界」这个问题在硬件和内核层的答案;那本书讲的是同一个问题在应用层的答案,可靠性也就跟着降了一档。
- 《当真》——第 18 章那个
fsync,是 PostgreSQL 整套 WAL 和组提交机制存在的理由。 - 《解释》/《求值》——如果这本书让你对「底下到底发生了什么」上了瘾,那两本会告诉你你写的语言本身是怎么跑起来的。
最后一句
这本书没打算让你成为内核开发者。它想给你的东西朴素得多:
一双能看见那道线的眼睛。
它一直在那儿。你写的每一行代码都活在它上面,你调用的每一个库函数迟早都要跨过它,你遇到的每一个「玄学」性能问题都是它在收账。
以前它是隐形的,所以那些现象看起来互不相干、无从下手。现在你能看见它了——于是那些现象会开始互相解释。
祝你在下一次撞见段错误、OOM、或者一个说不清的延迟尖刺时,能笑一下,然后知道该先敲哪条命令。