ROUNDOFF · 五卷 · 20 章

你写下 0.1 的那一刻,机器已经欠了你 5.55 × 10⁻¹⁸。
这个数小得可笑——所以大多数人对浮点数的全部认识,止步于「0.1 + 0.2 有点怪」。
可是 1991 年,同样的一次抹零让一枚拦截弹的窗口偏出 575 米
一份每笔都精确的一百万条流水,最自然的那种写法把账算差了 1.08 元
问题从来不是抹了多少,是它会不会被放大。

零基础不用未解释的术语与 numpy / mpmath 逐项核对 本机 C 编译器实测亮色

这本书只教一件手艺:看出误差会不会被放大,然后换一种写法。
放大倍数由两个互不相干的数相乘得来——
一个写在问题里(条件数,你改不了),一个写在你的算法里(稳定性,你能改,而且通常免费)。
分清这两件事,是这本书全部的价值。

这本书里那些不像真的、但都是当场算出来的数

  • 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

数轴不是连续的。你的每一个数,都被推到了最近的那根刻度上。

  1. 01你以为在算数,其实在往栅栏上靠0.1 的精确值、一张「你以为它是 X,其实它是 Y」的跨领域对照表,和 575 米★ 爱国者
  2. 02栅栏的形状ulp、机器 epsilon、2⁵³ 那道坎;以及为什么一半的 double 挤在 ±1 里★ 49.98%
  3. 03减法不产生误差它只是把早就存在的误差顶到了前排——一步丢掉八位有效数字★ 相消
  4. 04抹一次,和抹一千万次误差按 √n 长还是按 n 长,取决于抹零的方向是不是一致★ 温哥华股指
卷 II

放大

GAIN

误差不可怕,放大才可怕。放大倍数一半写在问题里,一半写在你的算法里。

  1. 05有些问题,天生就听不清条件数是问题自己的属性;改一个系数十亿分之零点六,十个根离开实数轴★★ 威尔金森
  2. 06好算法不给准答案后向稳定的定义,和那条把两半拼起来的公式:前向 ≲ κ × 后向★ e⁻⁵⁰ = 2041.83
  3. 07残差很小,答案全错残差 4.44e-16,答案错一半;换成零舍入的算术,照样错 30.7%★★ 希尔伯特 12
  4. 08同一份账,六种加法,六个答案最自然的写法差 1.08 元;教科书推荐的那种赢了,但赢得很可耻★★ 招牌
卷 III

改写

REWRITE

这本书的转折点:不改数据、不换语言、不加精度,只把式子换一种写法。

  1. 09求根公式是错的解 x² + 10⁹x + 1 = 0,公式给出的小根是 0;改一行,十六位全对★ 韦达定理
  2. 10方差算出了负数−16384;两种修法,以及一个反常识:Welford 其实不是最准的那个★★ −16384
  3. 11AI 里最常做的那一次改写softmax 给出 [NaN, NaN, NaN];以及为什么框架要提供 log_softmax★ log-sum-exp
  4. 12两个数相等,是什么意思0.1+0.2 和 0.3 其实是邻居;而 1e-9 的容差会把差 400% 的判成相等★ 隔几格
卷 IV

逼近

APPROACH

不再一步算完,改成一步步靠近。于是问题变成了:什么时候停。

  1. 13每一步把正确位数翻倍0.5 → 1.2 → 2.8 → 5.8 → 11.9 → 到顶。五步,而二分法要 52 步★ 二次收敛
  2. 14什么时候可以停下来三种听起来都合理的停机信号,每一种都会骗你;其中一种会死循环★ 停机判据
  3. 15永远不要求逆矩阵换两行的位置能把答案从全错救成全对;求逆慢两倍、残差差十一个数量级★ 主元
  4. 16最小二乘把条件数平方了正规方程等于把你的 double 悄悄降级成 float★ QR vs 正规方程
卷 V

换格

REGRID

双精度不是唯一的格子。换一种格子,换的是「把精度花在哪」。

  1. 17钱不能用 double同一门语言里两个四舍五入写法,对同一个数给出 2.68 和 2.67★ 整数分 / 十进制
  2. 18八位尾数怎么训出大模型bf16 的精度比 fp16 还差八倍,业界却全换了过去。这是一笔很清楚的交易★★ 范围 vs 精度
  3. 19同一份代码,两台机器,两个答案加一个编译开关,结果就变;改一下线程数,六个答案。全部在本机跑出来★★ 本机实测
  4. 20你手里那把尺子二十条自查表、两张决策表,和一个二十行的扰动实验★ 自查台

写给谁

  • 写给每天都在用浮点数、但从没系统学过它的人。这是个奇怪的处境:几乎所有程序员每天都在写 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