ROUNDOFF · 五卷 · 20 章
抹零
你写下 0.1 的那一刻,机器已经欠了你 5.55 × 10⁻¹⁸。
这个数小得可笑——所以大多数人对浮点数的全部认识,止步于「0.1 + 0.2 有点怪」。
可是 1991 年,同样的一次抹零让一枚拦截弹的窗口偏出 575 米;
一份每笔都精确的一百万条流水,最自然的那种写法把账算差了 1.08 元。
问题从来不是抹了多少,是它会不会被放大。
这本书只教一件手艺:看出误差会不会被放大,然后换一种写法。
放大倍数由两个互不相干的数相乘得来——
一个写在问题里(条件数,你改不了),一个写在你的算法里(稳定性,你能改,而且通常免费)。
分清这两件事,是这本书全部的价值。
这本书里那些不像真的、但都是当场算出来的数
- 1991 年,一套连续开机 100 小时的防空系统,内部时钟慢了 0.343 秒;来袭目标以 1676 米/秒飞行,跟踪窗口因此偏出 575 米。而单次误差是一亿分之九点五秒。(第 1 章)
- 全部 double 里,有 49.9756% 挤在 −1 和 1 之间。剩下那一半要覆盖到 1.8 × 10³⁰⁸。(第 2 章)
- 把二十次多项式的一个系数改动十亿分之零点六,二十个根里有十个当场离开实数轴。这个结果和 Wilkinson 1963 年算出来的前六位相同。(第 5 章)
- 解一个 12×12 的方程组:残差 4.44 × 10⁻¹⁶,而十二个分量里只有 4 个对到两位小数。改用完全精确的有理数算术重解——照样错 30.7%。(第 7 章)
- 一百万条分录,每一笔都精确,正确余额是 4321.55。六种求和写法给出六个答案,最自然的那种把账算差了 1.08 元。(第 8 章)
- 四个整数的方差,正确答案是 1.25。教科书那条公式算出 −16384。方差是平方和,它不可能是负的。(第 10 章)
- 用泰勒级数算
e⁻⁵⁰(真值 1.93 × 10⁻²²),加满 169 项,得到 2041.83。(第 6 章) - 同一份 C 源码、同一台机器、同一个编译器,只加一个
-ffast-math:累加一千万个 0.1 的结果从 999999.99983897537 变成 1000000.0000223046。(第 19 章) - int8 量化 4096 个权重,信噪比 41.78 dB;混进一个离群值,637 个权重被量化成 0。(第 18 章)
卷 I
栅栏
FENCE数轴不是连续的。你的每一个数,都被推到了最近的那根刻度上。
卷 II
放大
GAIN误差不可怕,放大才可怕。放大倍数一半写在问题里,一半写在你的算法里。
卷 III
改写
REWRITE这本书的转折点:不改数据、不换语言、不加精度,只把式子换一种写法。
卷 IV
逼近
APPROACH不再一步算完,改成一步步靠近。于是问题变成了:什么时候停。
卷 V
换格
REGRID双精度不是唯一的格子。换一种格子,换的是「把精度花在哪」。
写给谁
- 写给每天都在用浮点数、但从没系统学过它的人。这是个奇怪的处境:几乎所有程序员每天都在写
double,而这门学问基本只在数值分析专业课里出现。不需要任何数值分析基础,每个术语第一次出现时都会当场解释。 - 写给做 AI 的人。混合精度、loss scaling、master weights、量化、梯度归约、可复现训练——这些工程惯例每一条都是这本书里的一章。第 11、18、19 章尤其直接。
- 写给做后端和数据的人。对账差一分钱、方差算出负数、同一个查询两次结果不同、回归系数正负翻转——第 8、10、16、17 章讲的就是这些。
- 写给对「怎么判断一个结论可信」感兴趣的人。这本书最后会说明:条件数、后向误差、「残差小 ≠ 答案对」这套语言,在民调、A/B 测试、经济预测、复盘归因上一样成立,而那些地方几乎没人在用它。
- 数学不多。全书只需要「比较两个数的大小」和「相对误差」这两个概念。公式一律先说要满足哪几条,再推出来,不空降。
不打算做的事
- 不劝你别用浮点数。double 在绝大多数场合完全够用,这本书花了很多力气说明「哪九成可以放心不管」。最后一章的错误直觉就是「知道这些之后要到处加防御性检查」。
- 不教「怎么让数值更精确」。提高精度在这本书里几乎从来不是答案——第 3、5、7 章分别给出了「加精度没用」的三个理由。答案通常是换一种写法。
- 不用暗色模式。全站亮色。
- 不写「大概是这个量级」。正文里出现的每一个数字都是实测的:Node 24 / Python 3.14 / numpy 2.5.2 / mpmath 1.3.0 / 本机 clang。写到几位就是实测到几位。
- 不回避对自己不利的结论。「先加小的」这条流传很广的建议在第 8 章那份数据上赢了,但赢得很可耻,照写;Kahan 补偿求和在那份数据上输给了 Neumaier,照写;
-ffast-math在第 19 章那个例子上给出了更准的答案,也照写。
怎么读这本书
- 顺着读。卷 I 建立全部零件(一排不均匀的刻度、一次抹零、两种放大方式),卷 II 给出诊断工具,卷 III 到卷 V 都在用它们。跳着读会缺零件。
- 每章开头那个「▷ 猜一下差多少」,请真的先落一个答案。章末的「◇ 对账」会逐个解释每个错误选项的直觉是从哪来的——那部分往往比正确答案更有用。这本书的主题恰好就是「你对误差的估计总是错的」,所以这个环节本身在演示它自己。
- 每章末尾还有一个「◈ 换一种写法」。它是这本书的主线动作:不改数据、不加精度,只换写法。二十章下来,它会从一个技巧变成一个习惯。
- 每章有一个「⌨ 自己跑一遍」,给的是可以直接粘贴的几十行代码 + 已经跑出来的结果表 + 去哪儿跑。零依赖或者只依赖 numpy。最值得跑的是第 7 章(精确算术也救不回来)和第 19 章(编译一次就懂)。
- 第 5、7、8、10、18、19 章是招牌。时间有限的话先看这六章。
- 第 20 章可以先看。它是一张自查表和两张决策表,先看一遍能知道后面十九章各自在解决什么问题。
这些数字凭什么可信
- 先算,后写。正文里每一个数字都是从实测输出里抄下来的,不是反过来。
- 跨实现交叉核对。同一件事至少用两条独立的路算:一百万条分录的和用 BigInt 精确有理数算一遍、用 Python 的
math.fsum算一遍、用fractions.Fraction算一遍——三条路给出同一个位模式。希尔伯特矩阵用 numpy(LAPACK)、mpmath 60 位、精确有理数高斯消元各解一遍。 - 与公开的历史结果核对。第 5 章那个扰动多项式的复根,Wilkinson 1963 年给出的是 16.730 73 ± 2.812 62i,这台机器上 numpy 算出 16.730762 ± 2.812625i——前六位相同。
- 第 19 章的数字是真的编译出来的。本机 clang,三种编译选项,三份不同的输出。其中
-O0那一份和 Node、Python 的输出逐位相同。 - 验证器会告诉你它查了多少项,包括正文里的数字与独立计算的逐条交叉核对。
npm run verify:roundoff。