← 揭开黑盒:理解系统的调查课堂

第二章 · 计算机选择的数学 / 第五单元 · 机器里的数

一个数为什么加不上去?

你给一个计数器加了 1,显示却没变。第一章教我们先核对保存、读取和显示。现在把这些路径缩到最短:直接让浏览器算 (x + 1) - x。

预计用时:40 分钟。前置是加减法和指数表示:2 ** 53 表示 2 的 53 次方。

看清这个 1 在哪一步消失

打开 数的缝隙。保持增加量为 1,把指数从 52 拖到 53,再拖到 54。先写预测:若只是显示舍掉末位,再减掉原来的大数,应当重新显出 1;若计算结果已经舍入,再相减也无法取回它。

页面同时给出起点、精确整数目标、实际计算结果和相减后的增量。目标用 JavaScript 的 BigInt 整数运算生成,实际路径使用 Number。两者的表示方式不同,这正是用来核对的条件。

在当前格式下,指数 52 时增量是 1;53 时增量变成 0。发生变化的不只是显示出来的位数。

固定的位,怎样容纳大小相差很多的数

常见浮点格式把数组织成符号、有效数字与尺度。可以先类比科学计数法:1.234 × 10^8 与 1.234 × 10^-8 共用同样多的有效数字,实际刻度却相差很远。binary64 使用二进制,并在正常数范围提供 53 位有效二进制精度。

在 2^e 右侧、尚未到 2^(e+1) 的范围内,相邻可表示数的间隔是 2^(e−52)。因此:

起点右侧相邻间隔加 1 的位置
2^521正好到下一个可表示数
2^532正好在两者中间
2^544离原数更近

中间结果必须落到格式能够保存的值上。通常的最近舍入在正中间使用“取偶数”的规则,这里的 2^53 + 1 会落回 2^53。图画的是起点右侧;幂次边界左侧的间隔可能不同。

这也解释了为什么不能把“整数形式的 Number”直接当作任意大的精确整数。2^53 本身能精确表示,但附近的连续整数已经不能全部区分。

为什么不把精度无限加大

可以选择更大的固定格式或任意精度计算。但多出的位要占空间,运算可能需要更多指令,数据传输也会增加。某些任务还需要精确十进制语义,如按规则处理货币;整数最小单位、十进制类型及舍入规则会成为设计的一部分。

现代 CPU 与 GPU 对不同格式有不同支持。某种较低精度格式可能节省带宽或提供更高吞吐,但输出能否满足任务要求,要用误差与输入范围核对。硬件支持一种格式,不会自动替我们决定它是否合适。

把模型带到另一个输入

先离开滑块,预测 2^53 + 2 与 2^53 + 3 的结果。可以在浏览器控制台运行以下完整小程序:

const x = 2 ** 53;
console.log((x + 1) - x);
console.log((x + 2) - x);
console.log((x + 3) - x);
  1. 记录三个增量,再说明哪些值正好可表示。
    看答案

    参考:增量依次为 0、2、4。最后一项也是中点舍入,落到相邻可表示值中的另一侧。不能把本节记成“大数加小数总会丢掉小数”。

再提出一种任务,需要区分 9007199254740992 与 9007199254740993,例如记录编号。说明你会使用字符串、BigInt 或其他表示的理由,以及需要调整的接口。

本节可以在“表示格式决定哪些结果能留下”处停下。我们还没有讨论一串计算怎样累积误差,也没有完整展开溢出、非正规数、无穷与 NaN。下一节先追问最熟悉的 加法顺序。