编程语言研究书库

04 浮点数:0.1 + 0.2 的位级真相

04 浮点数:0.1 + 0.2 的位级真相

这一章讲什么

每个学 JavaScript 的人都见过这个笑话:0.1 + 0.2 === 0.3 返回 false, 加起来是 0.300000000000000041。网上把它当「JS 之怪」传,但原书第一句话就说: 这不是 bug,是 JavaScript 把所有数字都存成一种格式的直接后果2。 那个格式叫 IEEE 754 双精度浮点数,理解了它,你不但能解释这个笑话, 还能预判一类生产事故:对账差一分钱、ID 对不上、某次计算莫名 NaN。

本章(连同下一章)走完整条链:数字怎么变成字节(二进制、补码)→ 为什么整数不够用(浮点是什么)→ 64 个二进制位怎么分工(符号、指数(2 的多少次方)、尾数)→ 0.1+0.2 的逐位推演 → 这个格式的能力边界(EPSILON、NaN、安全整数)→ 逃出浮点的三条路(BigInt、Decimal、V8 的 SMI 快道,下一章讲)。

顶层全景:一个数字的 64 位工位

  ┌───┬───────────────┬──────────────────────────────────────────────────┐
  │ 1 │      11       │                       52                         │
  │位 │     位        │                       位                          │
  │符号│    指数       │                     尾数(有效数字)                │
  │ ± │  2 的多少次方  │      1.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx     │
  └───┴───────────────┴──────────────────────────────────────────────────┘
       值的正负      小数点漂在哪        实际的有效数字(53 位,首位的 1 白送)

  图说:64 位里没有「整数区」「小数区」,只有「符号 × 1.尾数 × 2^指数」;
        所以 0.5 和 500000000 占用完全相同的 64 位,精度却天差地别。

核心原理

3.1 先修地基:二进制与补码

二进制就是只拿 0 和 1 两个数字来计数,每个位置代表 2 的幂(十进制每个位置是 10 的幂)。 1011 = 1×2³ + 0×2² + 1×2¹ + 1×2⁰ = 8+2+1 = 113。8 个二进制位(也就是 1 字节) 能表示 2⁸ = 256 个值,因为 0 也要占一个位置,所以无符号范围是 0–2554。 这套「普通二进制」只能表示非负整数。

要表示负数,工业界的主流方案叫补码(2's complement):把最高位指定为符号位, 而负数的编码 = 正数的所有位取反、再加 15。它看起来古怪,但有一个绝妙的性质: 加一个数和加它的负数,用同一套电路就能得到 0(最高位的进位直接丢掉)。 书里拿 -1 做了演示:11111111 + 00000001 = 1 00000000,第 9 位溢出丢弃, 剩下全零6。补码不是谁拍脑袋的审美选择,是让加减法不用分情况讨论的工程答案。

补码还有个近亲叫偏置编码(biased encoding):存的时候先加一个固定偏置 (8 位就用 128),-128 存成 0,1 存成 129。它比较大小方便,但加减法要额外处理偏置, 所以只用在特殊场合7——浮点的指数区正是用的它,马上见到。

3.2 浮点是什么:科学计数法的二进制版

主线例子:把 123.45 存进 64 位。 先建立直觉:浮点(floating point)就是科学计数法, 只不过进制从 10 换成 2。十进制里 123.45 = 12345 × 10⁻²;浮点里任何一个数都拆成三件套8:

  • 符号:一个 bit,0 正 1 负;
  • 尾数(significand,也有人叫 mantissa):1.xxx 那个有效数字部分;
  • 指数:2 的多少次方——小数点「漂」到哪由它决定,这就是「浮点」名字的由来9。

64 位的分工原书给得很清楚:1 位符号、11 位指数、52 位尾数10。 指数区用偏置编码,偏置是 1023:真实的指数 -2 要存成 -2+1023 = 1021 (01111111101)11。为什么需要偏置?因为指数有正有负,而 11 位二进制本身存的是 无符号数——加 1023 这个偏移量,就把「-1022 到 +1023」搬进了「1 到 2046」, 比较和硬件处理都省事(这也是 3.1 节说补码之外还有偏置这条路的用处)。

现在解码 123.45,这是全书最值钱的 walked example:

  64 位 = 0 | 10000000101 | 1110110111001100110011001100110011001100110011001101

  ① 符号位 0 → 正数
  ② 指数区 10000000101 = 1029;1029 − 1023(偏置) = 6 → 乘 2^6 = 64
  ③ 尾数区解码 = 0.92890625;加上白送的首位 1 → 1.92890625
  ④ 合起来:1.92890625 × 64 = 123.45 ✓

(这四个数字全部来自原书的逐步解码,可直接复算12。) 注意第 ③ 步的「白送的首位 1」:规格化二进制科学计数法里,任何数都是 1. something × 2ⁿ, 小数点前的 1 必然存在——既然必然存在,存它就是浪费。IEEE 754 的设计者把这个 1 扔掉, 用 52 位的存储换到了 53 位的实际精度13。

3.3 0.1 + 0.2:逐位推演,误差(结果跟精确值的偏差)在哪一步产生

现在回答本章开头的问题。推演分三步,每步都有具体的位14:

第 ① 步:0.1 和 0.2 根本存不准。 把它们各自规格化成二进制:

  0.1 ≈ 1.1001100110011…(0011 无限循环)× 2^-4
  0.2 ≈ 1.1001100110011…(0011 无限循环)× 2^-3

像 1/3 在十进制里是 0.333… 一样,0.1 和 0.2 在二进制里都是无限循环小数; 而尾数只有 52 位,装不下无限——在相加之前,它们就已经各自被四舍五入了一次15。 还有个细节值得记住:0.2 明明是 0.1 的两倍,指数却不同(一个 -4 一个 -3), 因为它们分处 2 的幂的两侧16。

第 ② 步:对齐(把两个数的小数点摆到同一位置)指数再相加。 加法要求指数一致, 0.2 的指数大 1,于是 0.1 的尾数整体右移一位,最高位的有效数字被挤出去17:

  0.1 → 0.11001100110011…0(移位后)
  0.2 → 1.10011001100110…0
  和 → 10.01100110011001…110

第 ③ 步:再规格化、再舍入,误差第二次进场。 和的小数点左边变成了 10, 要重新规格化成 1.xxx × 2ⁿ,指数加 1;而结果有 54 位,尾数只有 52 位—— 多出来的尾巴再次四舍五入,进位顺着 0011 的循环一路传播18。

最终存入的是 1.00110011…0100 × 2^-2,而真正的 0.3 应该是 1.00110011…0011 × 2^-2—— 最后几位不同,换算回十进制,差就是 0.0000000000000000419。

结论一句话:这不是 JavaScript 的怪癖,是「十进制小数放进二进制容器」的数学宿命; Python、Java、C++,凡用 IEEE 754 的语言,结果一字不差20。

3.4 能力边界:EPSILON、NaN、和安全整数天花板

比「差不多相等」:Number.EPSILON。 既然浮点误差不可避免, a === b 经常不是你要的。语言给了一个常量 Number.EPSILON, 值是 2⁻⁵² ≈ 2.22×10⁻¹⁶——两个可表示的数之间能有的最小差距21。 所以判等改成「差得够不够小」:Math.abs(a - b) < Number.EPSILON。 但书里立刻警告:EPSILON 必须按数的量级缩放——比较 0.1 和 0.3 时 2.22×10⁻¹⁶ 够用,比较两个亿级大数时,可表示的间距远大于这个值, 直接用会误判。正确写法是让 epsilon 乘上两个数里较大的绝对值22。

NaN 其实是 9 千万亿个值。 指数全 1、尾数非 0 的编码全部判为 NaN(Not a Number, 「不是数」)——这样的编码在 Binary64 里有 2⁵³−2 = 9,007,199,254,740,990 个, 而 JavaScript 把它们全部显示成同一个 NaN23。无穷大则是指数全 1、尾数全 0, 分正负两个24。整个格式的动态范围惊人:最小非零 5×10⁻³²⁴,最大 1.8×10³⁰⁸, 但代价是全范围只有约 2⁶⁴−2⁵²+3 个可表示的值,而且数越大,相邻两个可表示数之间的 缝隙越大25——这正是下一节的灾难根源。

安全整数天花板:2⁵³−1。 尾数有 53 位精度,意味着「能精确表示的整数」到 2⁵³−1 = 9,007,199,254,740,991 为止。超过它,可表示的数开始跳号:

  console.log(9007199254740992);  // 2^53    → 9007199254740992 ✓
  console.log(9007199254740993);  // 2^53 + 1 → 9007199254740992 ✗ 被吞了!
  console.log(9007199254740994);  // 2^53 + 2 → 9007199254740994 ✓

(三个数与结果均来自原书实测26。)9,007,199,254,740,993 这个数字 在 JavaScript 里字面意义上不存在——引擎会把它存成相邻的偶数27。 没有报错,没有警告,就是个看起来无害的错数。

和数据库的冲突。 很多数据库用 uint64(无符号 64 位整数)做主键, 上限 18,446,744,073,709,551,615——比 JavaScript 的安全整数上限大两千多倍28。 后果是个经典生产事故:后端把一个 uint64 ID 序列化(把内存里的值转成可传输的文本)成 JSON(一种通用的文本数据格式)数字传给前端, JavaScript 一解析就静默改号。书里的对策很现实:超过安全整数的值, 要么按字符串传,要么用 BigInt(下一章)29。

3.5 精度问题什么时候真的要命

浮点误差大多数时候无所谓:动画位移差 10⁻¹⁶ 像素,没人看得见。 书里把话说得很直白:要命的是钱。货币计算里,每次加减乘除丢的那一点点, 聚合起来就是真金白银的对不上账30。19.99 美元的商品按 8.25% 计税, 19.99 * 0.0825 得到 1.6491749999999998 而不是 1.64917531—— 发票上的每一位都是用户能看见的。 这一节不给「银行家舍入」之类的完整方案(完整的答案在下一章的 BigInt/Decimal), 只立一条判别标准:这个数的误差有没有人(或另一个系统)会当真?会,就别用浮点。

3.1b 补码的完整对照:为什么 −1 是全 1

把 8 位补码的前几个值列全,规律就一目了然32:

符号位其余 7 位十进制值
000000000
000000011
000000102
000000113
11111111−1
11111110−2
11111101−3

正数按普通二进制数;负数是「取反加一」的结果,而且单调性保持: 1111111(−1)> 1111110(−2),二进制的大小比较和十进制值的大小比较一致—— 这正是偏置编码做不到、补码能独占鳌头的原因33。

书里最直观的一段演示是把 −1 和 1 加起来,逐位列出进位34:

  位:      7 6 5 4 3 2 1 0
  -1:      1 1 1 1 1 1 1 1
   1:      0 0 0 0 0 0 0 1
  进位:  1 1 1 1 1 1 1
  结果:    0 0 0 0 0 0 0 0   ← 第 9 位进位溢出,直接丢弃

从右往左:第 0 位 1+1=10,写 0 进 1;接下来每一位都是 1+0+进位 1=10, 写 0 进 1;最后的进位落到第 8 位,超出了 8 个位的容量,丢弃—— 剩下全是 0。这套「溢出即丢弃」的算术电路,让带符号数和无符号数 共用同一套硬件加法器,是补码被选中的根本理由35。

3.1c 字节序:一个数拆成多个字节时,谁先谁后

数字一大就要跨字节。书里给的例子:34,375 的 16 位二进制拆成两个字节是 0x86(高位)和 0x47(低位),「谁先摆上桌面」有两种约定36:

  • 大端(big-endian,网络字节序):高位在前,写作 86 47—— 像我们从左到右写数字,好读;
  • 小端(little-endian):低位在前,写作 47 86—— 像把 34,375 读成「5 个一、7 个十、3 个百……」倒着来。

大多数现代系统内部用小端(硬件优化方便),网络传输统一用大端 (约定好,人人可解)37。书里为清晰起见示例统一用大端; 而我们日常几乎不碰字节序——这一节的意义在于: 「一个数」和「它在内存/线路上的字节」之间,隔着一层约定。 IEEE 754 讲的正是这层约定在 64 位上的精确版本。

顺带记住书里的一句提醒:光给一串位(比如 11111111)而不说编码方式, 你根本无法知道它表示 255、−1 还是别的什么——位不携带含义,编码才携带38。

3.2b 为什么是「浮点」:固定点做不到的事

「浮点」这个名字说的是小数点的位置不固定,随指数漂移。 对比之下,定点(fixed-point)表示法把小数点钉死:比如约定 8 位整数 + 4 位小数, 就只能表示 12345678.1234 这种形态——整数部分一大,小数部分就被挤没, 反过来也一样39。浮点用「1.尾数 × 2^指数」的结构, 让 0.000000000000123 和 123,000,000,000,000 用同一套 64 位装下, 代价是有效数字永远只有 53 位:数越大,绝对精度越差40。 123.45 那组解码数字(1.92890625 × 64)就是这个漂移的实证: 指数 +6 把小数点右推了 6 位。

3.2c 尾数逐位解码:52 位怎么变成 53 位精度

隐含的 leading 1 白送之后,剩下的 52 位每一位代表 2 的负幂: 第 1 位是 2⁻¹(0.5),第 2 位是 2⁻²(0.25),第 3 位是 2⁻³(0.125)…… 一直到第 52 位的 2⁻⁵²41。书里给了一个可复算的样例: 如果 52 位按「010101…」交替置位,总和是

  1 + 2⁻² + 2⁻⁴ + 2⁻⁶ + … + 2⁻⁵² ≈ 1.3333…

再乘上指数给出的 2 的幂和符号,就是最终数值42。 「52 位存储、53 位精度」的技巧全部家当就是这句: 规格化二进制数小数点前必是 1,所以不用存43。

3.3b 0.1 + 0.2 的加法全程:三步之后,错在哪个比特

3.3 节给了结论,这里把位级的每一步走完,全部数字可在原书表 3.3–3.5 里核对44:

起点: 0.1 与 0.2 存入后的尾数都是 1.1001100…(0011 无限循环), 0.1 的指数是 01111111011,0.2 的是 01111111100——后者恰好大 1。 注意两个尾数看起来相同而指数不同,因为 0.2 跨过了 2 的幂边界: 0.1 ≈ 1.6×2⁻⁴,0.2 ≈ 1.6×2⁻³,就像十进制里 0.5 和 5 的指数不同45。

移位对齐: 把 0.1 的尾数右移一位、指数加 1,两者对齐: 0.1 变成 0.1100110…0,0.2 保持 1.1001100…46。 右移挤掉的那一位,是进入加法前的第三次信息损失(存入时各舍入过一次)。

逐位相加: 从右往左做二进制加法,进位沿 0011 循环一路传导, 得到 10.0110011…110——小数点左边出现 10,不再规格化47。

再规格化 + 再舍入: 小数点右归一位、指数加 1;结果是 54 位, 尾数只留 52 位,末尾的 10 被四舍五入成 11 还是 00, 取决于第 53 位——进位再一次顺循环传导48。

最终落盘的 1.00110011…0100 × 2⁻² 与真值 0.3 的 1.00110011…0011 × 2⁻² 差在最后几位,十进制里就是 0.30000000000000004 与 0.3 的距离49。 全链条一共四处损失:两次存入舍入、一次对齐移位、一次结果舍入—— 没有一处是「加法算错」,全是「容器装不下」50。

3.4b Infinity、负无穷与 NaN 的位模式

指数全 1、尾数全 0 的两个位模式分给正负无穷:

  +∞ = 0 11111111111 0000…0
  -∞ = 1 11111111111 0000…0

指数全 1、尾数非 0 的,全部归 NaN51。1/0 得 +∞, Number('abc') 得 NaN,Infinity - Infinity 也得 NaN—— 「无穷减无穷没有定义」这条数学规则,在二进制里就是「落入 NaN 编码区」。 而 Binary64 全境的可用值只有约 2⁶⁴−2⁵²+3 个, 摊到 ±1.8×10³⁰⁸ 的范围上,大数附近的「间距」大得惊人: 在 2⁵³ 附近,相邻两个可表示数之间隔 2;到 2⁶⁰,间隔变成 12852。 「数越大,加一越可能什么都没发生」就是这个几何的直观。

3.4c Math 对象与它的脾气

Math 不是类型,是常量与函数的集合处(书里叫它 namespace): Math.PI、Math.E;round/floor/ceil/abs/pow/sqrt, 三角函数用弧度,Math.random() 返回 0 到 1 之间的伪随机数53。 两个脾气要记:所有 Math 方法只吃 Number, 喂不进去的输入返回 NaN 而不是抛错——「静默失败」的浮点版; 以及 Math.round 的「逢半向上」不是银行家舍入, 钱的场景别指望它54。

3.4d 为什么是 2 的 53 次方:安全整数的二进制解释

「最大安全整数是 2⁵³−1」是个容易被当成咒语背的数字,它的来历其实一句话: 尾数区 52 位,加上白送的 leading 1,一共 53 位有效二进制数字; 53 位恰好能无歧义地覆盖的所有整数,到 2⁵³−1 为止55。 推导走一遍:一个规格化二进制数能精确表示的整数,其位数不能超过有效数字的位数; 53 位有效数字 ⇒ 最多精确表示 53 位的整数;53 位整数的最大值是 2⁵³−1 (52 位元后接 52 个 0 再减 1 的形态,换成十进制就是 9,007,199,254,740,991)。 超过它,相邻可表示数之间的距离变成 2,奇数开始「被四舍五入到隔壁」, 2⁵³+1 就此消失56。安全整数的「安全」两个字说的就是这个: 在它以内,每个整数都有专属的位模式,一一对应,谁也不挤谁。

这也解释了为什么 EPSILON 是 2⁻⁵²:1 和「比 1 大的下一个可表示数」之间, 差的正是尾数最后一位的一步——2⁻⁵²57。同一个 52/53 的分界线, 一头是「最大的整数」,一头是「最小的差」,同一个机制的两张脸。

3.4e 浮点误差的「剂量」问题

原书在「Limitations of floating-point」一节把话说得很克制: 浮点运算——按 64 位二进制小数做的一切计算——「只能逼近正确值」,这个损失「叫精度丢失(loss of precision)」; 对大多数场景无所谓,但钱会把它放大——每次加减乘除丢一点, 聚合成账面上的实损58。判断标准书里其实已经给了,只是我们把它提亮: 这个数的误差有没有人(或另一个系统)会当真? 没人当真(动画帧位移、科学计算的中间量),浮点随便用; 有人当真(价格、余额、数量),立刻换 3.5 节的方案59。 「不谈剂量谈毒性」在这里同样成立:0.1+0.2 的误差剂量是 4×10⁻¹⁷, 在粒子物理里是灾难,在购物车总额里 invisible——直到它撞上 「展示给用户的每一位」这条边界60。

3.0b 章首的三个坐标:这一章在原书里的位置

原书第 3 章开篇把全章的地理交代得很清楚,这里照录坐标61:

  • 语言只给两种数字类型:Number 和 BigInt——别的语言让你选整数/小数、 有符号/无符号、8 位/64 位,JavaScript 一概不让你选62;
  • 一个决定,一串后果:0.1+0.2≠0.3、金融计算差一分钱、 数过 9,007,199,254,740,991 不再可靠、V8 偷偷把小整数区别对待—— 全是「所有数字都是双精度浮点」这一个决定的 downstream63;
  • 全章走三个角度:IEEE 754 怎么定义浮点、BigInt 怎么当逃生舱、 V8 怎么给小整数开快道;外加一个预告——正在提案的 Decimal 「可能终于让 JavaScript 能精确计算 0.1+0.2」64。

记住这套坐标,读后面任何一段都能回答「我现在在全章的哪一块」。

3.1e 与第 2 章的隐秘呼应:数字也是一种编码

读完本章回头看,第 2 章的那套语言在第 3 章全部重演了一遍, 这个对称不是巧合,是原书的有意安排65:

  • 字符串是「16 位整数的序列」,数字是「64 位的特定布局」—— 两者都是「位 → 含义」的编码问题,只是字符编码管符号,数值编码管量;
  • 字符串有 UTF-32/16/8 三种容器格式,数字有 Binary64/Decimal128/BigInt 三种容器——都在「定宽简单」与「变宽通用」之间做权衡;
  • 字符串事故来自「编码假设错位」(Shift-JIS 当 UTF-8), 数字事故来自「精度假设错位」(uint64 ID 当 Number)—— 都是跨系统边界时,两边对「容器」的理解不一致。

所以这本书教的不是两个孤立的知识点,而是一双眼睛: 看到任何「值」,都问一句「它在容器里怎么躺着的、跨边界时对方怎么解读的」。 第 2 章的字符串、第 3 章的数字、第 5 章的对象(隐藏类), 是同一双眼睛的三次使用。

3.3c 把 0.1+0.2 讲给别人听:一分钟版本

合上这一章,试着用这个顺序复述 0.1+0.2,能讲下来才算真懂66:

  1. 0.1 和 0.2 写成二进制都是无限循环(像 1/3 的 0.333…), 64 位装不下 → 存入时就各自舍入了一次;
  2. 相加前要对齐指数(像十进制对齐小数点), 0.1 的尾数右移,又损失一位;
  3. 加完是 54 位,尾数只有 52 位,再舍入一次, 进位顺着循环传导,末几位变了;
  4. 落盘的位串换算回十进制,就是 0.30000000000000004;
  5. 这不是 bug:IEEE 754 的每种语言、每个实现,结果一字不差。

五步,四处舍入,零处「算错」。浮点的全部故事,就是「装不下」三个字。

讲给别人听时最常卡住的追问有两个,提前备好答案。 问:「为什么不干脆用十进制存?」——答:这就是 Decimal128 在做的事 (见 05 章 3.2 节),代价是更宽的容器和更慢的运算; 问:「为什么别的语言没这毛病?」——答:都有,Python、Java、C++ 结果一字不差,只是有的语言默认容器不是浮点、或默认显示位数把它藏住了67。

作者的判断与证据

有证据的判断:

  • 123.45 的逐步解码、0.1/0.2 的指数与循环模式、三步加法推演,全部给出具体位串, 可手算复核68;「所有 IEEE 754 语言结果一致」是可实验验证的论断。
  • 2⁵³ 附近三个整数的输出是实测结果69。

作者的主张:

  • 「钱必须用别的方案算」是立场鲜明的实践主张,书里给出的选项(下一章展开) 本身也是「还在演进中」的方案(如 Decimal 尚未定型)。
  • 「Number.EPSILON 是最被忽视的特性之一」属作者评价。

判断(我们的,不是书里的): 用 0.1+0.2 一个例子贯穿整章的教法非常划算—— 它同时教会读者「浮点是相对精度」「误差来自两次舍入」「比较要按量级缩放容差」 三件事;相比之下,直接背「浮点数有误差」的教训几乎留不住。 如果错,会错在: 如果读者只记住「0.1+0.2 会出错」而没记住「按量级比较」, 这一章就退化成又一个梗图;教法的效果依赖读者动手复算,而不是读完点头。

边界与局限

  • 本章只覆盖了 IEEE 754 的「够用」部分;舍入模式、次正规数(subnormal)、 异常标志这些规范内容原书明说「 irrelevant 于 JavaScript」而跳过70。
  • 「大多数语言结果一致」有个历史脚注:某些旧语言/编译器在中间计算用 更高精度(扩展双精度),结果可能不同——原书未提,属实现史细节。
  • 安全整数的处理方式(转字符串)是权宜之计;JSON 规范本身没有 BigInt 类型, 跨系统传大整数至今仍是个协议(双方约定好的交换格式)设计问题。

可带走的

  1. Number = IEEE 754 双精度浮点:1 符号 + 11 指数(偏置 1023)+ 52 尾数(实际 53 位)。
  2. 0.1+0.2≠0.3 的完整链条:无限循环二进制 → 存入前舍入 → 对齐指数移位 → 结果再舍入。
  3. 判定浮点相等:差值 < Number.EPSILON × max(|a|,|b|),永远不要裸用 EPSILON。
  4. NaN 有 9,007,199,254,740,990 个编码,显示成一个;Infinity 有两个编码。
  5. 安全整数上限 2⁵³−1 = 9,007,199,254,740,991;uint64 ID 必须按字符串或 BigInt 传。
  6. 规格化二进制的小数点前必是 1,所以 52 位存储换 53 位精度。
  7. 数越大缝隙越大:大数上的「加 1」可能什么都不发生。
  8. 钱的计算别赌浮点;判别标准是「误差会不会有人当真」。

原文地图

主题原书章原文位置
0.1+0.2 与「不是 bug」3 Numberstext/13-ch03-3-numbers.txt:51(搜「0.30000000000000004」) · :59(搜「double-precision floating-point values」)
Number 的规范定义同上text/13-ch03-3-numbers.txt:77(搜「conforming to IEEE 754」)
IEEE 754 标准与高价下载同上text/13-ch03-3-numbers.txt:90(搜「over a hundred US dollars」)
二进制与 0–255同上text/13-ch03-3-numbers.txt:189(搜「256 possible numeric values」)
补码:取反加一同上text/13-ch03-3-numbers.txt:195(搜「sign bit」) · :201(搜「complement), and add 1」)
浮点三件套同上text/13-ch03-3-numbers.txt:565(搜「scientific notation」)
64 位分工与偏置 1023同上text/13-ch03-3-numbers.txt:589(搜「eleven exponent bits」) · :601(搜「bias of 1023」)
53 位精度的技巧同上text/13-ch03-3-numbers.txt:641(搜「53 bits of precision」)
123.45 逐步解码同上text/13-ch03-3-numbers.txt:704(搜「decode the number 123.45」) · :743(搜「1.92890625」)
0.1/0.2 的循环模式同上text/13-ch03-3-numbers.txt:828(搜「repeating pattern 1001」)
对齐指数同上text/13-ch03-3-numbers.txt:834(搜「shift 0.1's significand」)
「每个语言都一样」同上text/13-ch03-3-numbers.txt:879(搜「Every language using IEEE 754」)
Number.EPSILON 与量级缩放同上text/13-ch03-3-numbers.txt:1007(搜「smallest difference」) · :1029(搜「scaled based on the magnitude」)
NaN 的 9,007,199,254,740,990 个值同上text/13-ch03-3-numbers.txt:1085(搜「9,007,199,254,740,990」)
5e-324 到 1.8e308、缝隙变大同上text/13-ch03-3-numbers.txt:1101(搜「5e-324」)
最大安全整数同上text/13-ch03-3-numbers.txt:1107(搜「maximum safe integer」)
9,007,199,254,099,3 不存在同上text/13-ch03-3-numbers.txt:1132(搜「literally cannot exist」)
uint64 主键冲突与字符串方案同上text/13-ch03-3-numbers.txt:1138(搜「2,000 times larger」)
货币误差聚合同上text/13-ch03-3-numbers.txt:1052(搜「lose precision」)

Footnotes

  1. 出处:「3 Numbers」第 51 段(text/13-ch03-3-numbers.txt:51,搜「0.30000000000000004」)。三个 console.log 的输出:和为 0.30000000000000004,字面量 0.3 显示为 0.3,严格相等为 false。 ↩

  2. 出处:「3 Numbers」第 59 段(text/13-ch03-3-numbers.txt:59,搜「direct consequence」):「It's not a bug. It's a direct consequence of how JavaScript chose to represent all numbers as double-precision floating-point values.」 ↩

  3. 出处:「3 Numbers」第 154 段(text/13-ch03-3-numbers.txt:154,搜「Binary: 1011」)。「每个位置代表 2 的幂」在同文件第 140 段(搜「powers of 2」)。 ↩

  4. 出处:「3 Numbers」第 189 段(text/13-ch03-3-numbers.txt:189,搜「256 possible numeric values」)。「regular binary layout(普通二进制布局)」这个术语在同段。 ↩

  5. 出处:「3 Numbers」第 195 段与第 195 段(text/13-ch03-3-numbers.txt:195,搜「sign bit」;:201 搜「invert all its bits」)。「2's complement」一名出自同段。 ↩

  6. 出处:「3 Numbers」第 298–445 段(text/13-ch03-3-numbers.txt:298,搜「Table 3.2」);「11111111 + 00000001 = 0(丢弃进位)」的逐位列在同文件第 428–440 段(搜「overflows beyond 8 bits」)。 ↩

  7. 出处:「3 Numbers」第 458 段(text/13-ch03-3-numbers.txt:458,搜「biased encoding」)。「比较大小方便、算术不自然」的权衡在同段。 ↩

  8. 出处:「3 Numbers」第 565 段(text/13-ch03-3-numbers.txt:565,搜「scientific notation」)。 ↩

  9. 出处:「3 Numbers」第 577–583 段(text/13-ch03-3-numbers.txt:577,搜「sign bit」;:583 搜「significand」)。「尾数也叫 mantissa,两词通用」见第 583 段。 ↩

  10. 出处:「3 Numbers」第 589 段(text/13-ch03-3-numbers.txt:589,搜「eleven exponent bits」)。「Binary64 / double-precision」的名目见第 595 段(搜「double-precision」)。 ↩

  11. 出处:「3 Numbers」第 601 段(text/13-ch03-3-numbers.txt:601,搜「bias of 1023」)。「-2 存成 1021」的算例在同段。 ↩

  12. 出处:「3 Numbers」第 704–746 段(text/13-ch03-3-numbers.txt:711,搜「10000000101」;:740 搜「26 = 64」;:743 搜「1.92890625」;:746 搜「123.45」)。 ↩

  13. 出处:「3 Numbers」第 641 段(text/13-ch03-3-numbers.txt:641,搜「53 bits of precision while only using 52 bits」)。「规格化数必是 1.something」见第 647 段(搜「leading digit」)。 ↩

  14. 出处:「3 Numbers」第 775–996 段(text/13-ch03-3-numbers.txt:775,搜「Table 3.3」)。推演三步分别对应:存入前舍入(第 828 段)、对齐指数(第 834–864 段)、结果再舍入(第 873 段与第 955–995 段)。 ↩

  15. 出处:「3 Numbers」第 828 段(text/13-ch03-3-numbers.txt:828,搜「forced to round them to fit in 52 bits」)。 ↩

  16. 出处:「3 Numbers」第 822 段(text/13-ch03-3-numbers.txt:822,搜「crosses a power-of-two boundary」)。 ↩

  17. 出处:「3 Numbers」第 834 段(text/13-ch03-3-numbers.txt:834,搜「shift 0.1's significand right by one」)。移位后的两行二进制见表 3.4(第 840 段起)。 ↩

  18. 出处:「3 Numbers」第 873 段与第 961–995 段(text/13-ch03-3-numbers.txt:873,搜「accumulating more tiny errors」;:961 搜「Normalize」;:964 搜「Round」)。 ↩

  19. 出处:「3 Numbers」第 976–995 段(text/13-ch03-3-numbers.txt:976,搜「1.0011001100110011001100110011001100110011001100110100」;:989 搜「1.0011001100110011001100110011001100110011001100110011」)。两者之差换算成十进制即 0.30000000000000004 与 0.3 的距离,见第 995 段(搜「last handful of bits」)。 ↩

  20. 出处:「3 Numbers」第 879 段(text/13-ch03-3-numbers.txt:879,搜「Every language using IEEE 754」)。 ↩

  21. 出处:「3 Numbers」第 1007 段(text/13-ch03-3-numbers.txt:1007,搜「smallest difference between two representable numbers」)。almostEqual 的用法示例在同文件第 1020–1023 段。 ↩

  22. 出处:「3 Numbers」第 1029 段与等比缩放版实现(text/13-ch03-3-numbers.txt:1029,搜「scaled based on the magnitude」;实现在第 1036–1040 段,搜「equalish」)。 ↩

  23. 出处:「3 Numbers」第 1085 段(text/13-ch03-3-numbers.txt:1085,搜「9,007,199,254,740,990」)。「全显示成同一个 NaN」在同段。 ↩

  24. 出处:「3 Numbers」第 1071–1080 段(text/13-ch03-3-numbers.txt:1077,搜「01111111 11110000」)。两个无穷大的位模式分别见第 1077 与第 1080 段。 ↩

  25. 出处:「3 Numbers」第 1101 段(text/13-ch03-3-numbers.txt:1101,搜「gaps between representable numbers」)。 ↩

  26. 出处:「3 Numbers」第 1114–1122 段(text/13-ch03-3-numbers.txt:1114,搜「9007199254740992」)。 ↩

  27. 出处:「3 Numbers」第 1132 段(text/13-ch03-3-numbers.txt:1132,搜「literally cannot exist」)。原话即如此,还带着感叹号。 ↩

  28. 出处:「3 Numbers」第 1138 段(text/13-ch03-3-numbers.txt:1138,搜「18,446,744,073,709,551,615」)。「大两千多倍」在同段(搜「over 2,000 times larger」)。 ↩

  29. 出处:「3 Numbers」第 1138 段(text/13-ch03-3-numbers.txt:1138,搜「stored as strings」)。 ↩

  30. 出处:「3 Numbers」第 1052 段(text/13-ch03-3-numbers.txt:1052,搜「quite large sums of money」)。 ↩

  31. 出处:「3 Numbers」第 1627–1631 段(text/13-ch03-3-numbers.txt:1627,搜「19.99」;:1631 搜「1.6491749999999998」)。 ↩

  32. 出处:「3 Numbers」表 3.1(text/13-ch03-3-numbers.txt:207,搜「2’s complement Encoding」)与第 189–195 段。 ↩

  33. 出处:「3 Numbers」第 458–464 段(text/13-ch03-3-numbers.txt:458,搜「biased encoding」):偏置方案「比较大小方便,但算术不自然」。 ↩

  34. 出处:「3 Numbers」表 3.2(text/13-ch03-3-numbers.txt:304,搜「Binary addition showing why -1」)与第 428–440 段的逐步进位说明。 ↩

  35. 出处:「3 Numbers」第 445 段(text/13-ch03-3-numbers.txt:445,搜「without special cases」):「同样的算术处理正负数,不用分情况、不加额外复杂度。」 ↩

  36. 出处:「3 Numbers」第 491–500 段(text/13-ch03-3-numbers.txt:491,搜「0x86」):34,375 拆成 0x86 与 0x47 的例子。 ↩

  37. 出处:「3 Numbers」第 505 段(text/13-ch03-3-numbers.txt:505,搜「little-endian is easier」):「大多数现代系统内部小端、网络传输大端,因为小端对硬件优化更友好。」 ↩

  38. 出处:「3 Numbers」第 545–552 段(text/13-ch03-3-numbers.txt:551,搜「impossible to know」):只看位序列不知道最高位是不是符号位,解读需要额外的编码信息。 ↩

  39. 出处:「3 Numbers」第 698 段(text/13-ch03-3-numbers.txt:698,搜「fixed-point」):定点表示「留 8 位整数 4 位小数,就只能是 12345678.1234 这种形态」。 ↩

  40. 出处:「3 Numbers」第 757 段(text/13-ch03-3-numbers.txt:757,搜「regardless of the number's magnitude」):「同样 53 位有效数字,相对精度一致,绝对精度天差地别。」 ↩

  41. 出处:「3 Numbers」第 653–668 段(text/13-ch03-3-numbers.txt:659,搜「b1 represents」):52 位从 b1 到 b52,各占 2⁻¹ 到 2⁻⁵²。 ↩

  42. 出处:「3 Numbers」第 673–690 段(text/13-ch03-3-numbers.txt:673,搜「0101010101」):交替置位算出约 1.3333,再乘 2 的幂得最终值。 ↩

  43. 出处:「3 Numbers」第 647 段(text/13-ch03-3-numbers.txt:647,搜「leading digit」):「规格化二进制科学计数法里,小数点前那一位永远是 1——它不可能 0,也不可能 ≥2。」 ↩

  44. 出处:「3 Numbers」表 3.3–3.5(text/13-ch03-3-numbers.txt:775,搜「Table 3.3」;:840 搜「Table 3.4」;:885 搜「Table 3.5」)。 ↩

  45. 出处:「3 Numbers」第 822 段(text/13-ch03-3-numbers.txt:822,搜「crosses a power-of-two boundary」):0.2 是 0.1 的两倍却指数不同,「就像科学计数法:0.1 ≈ 1.6×2⁻⁴,0.2 ≈ 1.6×2⁻³」。 ↩

  46. 出处:「3 Numbers」第 834 段与表 3.4(text/13-ch03-3-numbers.txt:834,搜「shift 0.1's significand right by one」)。 ↩

  47. 出处:「3 Numbers」表 3.5 与第 923–950 段(text/13-ch03-3-numbers.txt:923,搜「Step-by-step from right to left」;进位行见 :905)。 ↩

  48. 出处:「3 Numbers」第 955–995 段(text/13-ch03-3-numbers.txt:961,搜「Normalize」;:964 搜「Round」;「第 53 位」的取舍见第 976–989 段两行位串的对比)。 ↩

  49. 出处:「3 Numbers」第 976–995 段(text/13-ch03-3-numbers.txt:976,搜「1.0011001100110011001100110011001100110011001100110100」;精确 0.3 见 :989)。差值换算见第 995 段(搜「last handful of bits」)。 ↩

  50. 出处:「3 Numbers」第 763–873 段的整个推演链(text/13-ch03-3-numbers.txt:763,搜「classic example」)。「Every language using IEEE 754 has this same behavior」见第 879 段。 ↩

  51. 出处:「3 Numbers」第 1071–1094 段(text/13-ch03-3-numbers.txt:1077,搜「01111111 11110000」):两个无穷的位模式与 NaN 的两个样例字节序列。 ↩

  52. 出处:「3 Numbers」第 1101 段(text/13-ch03-3-numbers.txt:1101,搜「gaps between representable numbers」):「缝隙随数值变大而变大。」2⁵³ 附近间隔 2、2⁶⁰ 附近间隔 128 为机制推论(间隔 = 2^(指数−52))。 ↩

  53. 出处:「3 Numbers」第 1159–1258 段(text/13-ch03-3-numbers.txt:1174,搜「Constants」;:1195 搜「Operations」)。 ↩

  54. 出处:「3 Numbers」第 1262 段(text/13-ch03-3-numbers.txt:1262,搜「return NaN for invalid inputs」)。「Math 方法只吃 Number」在同段。补充(不在书里,来自通用知识):Math.round 逢 .5 向正无穷方向进位,与「银行家舍入」(逢半取偶)不同。 ↩

  55. 出处:「3 Numbers」第 1107 段(text/13-ch03-3-numbers.txt:1107,搜「maximum safe integer」):「最大安全整数 2⁵³−1 = 9,007,199,254,740,991;超过这一点,不是每个整数都能被精确表示。」53 位有效数字的推导见第 641 段(搜「53 bits of precision」)与本章 3.2c 节。 ↩

  56. 出处:「3 Numbers」第 1114–1132 段(text/13-ch03-3-numbers.txt:1114,搜「9007199254740992」):2⁵³ 安全、2⁵³+1 不存在、2⁵³+2 安全,三个输出照抄原书。 ↩

  57. 出处:「3 Numbers」第 1007 段(text/13-ch03-3-numbers.txt:1007,搜「2-52」):EPSILON = 2⁻⁵² ≈ 2.220446049250313e-16,「两个可表示数之间的最小差」。 ↩

  58. 出处:「3 Numbers」第 1052 段(text/13-ch03-3-numbers.txt:1052,搜「lose precision」):「这样的运算被说成丢失精度;货币场景下,小舍入误差聚合起来可能是大钱的对错。」 ↩

  59. 出处:「3 Numbers」第 1052 段(text/13-ch03-3-numbers.txt:1052,搜「not consequential」):「对许多用例,这个损失无关紧要。」判别标准是我们的提炼。 ↩

  60. 出处:「3 Numbers」第 1620–1631 段(text/13-ch03-3-numbers.txt:1620,搜「invoice calculation」):发票例子即「展示给用户的每一位」的边界。 ↩

  61. 出处:「3 Numbers」第 38–65 段(text/13-ch03-3-numbers.txt:38,搜「just two numeric types」):章节结构与三个角度见第 65 段(搜「three angles」)。 ↩

  62. 出处:「3 Numbers」第 38 段(text/13-ch03-3-numbers.txt:38,搜「8-bit and 64-bit」):「多数语言让你在整数与小数、有符号与无符号、8 位与 64 位之间选;JavaScript 只给你 Number 和 BigInt 两种。」 ↩

  63. 出处:「3 Numbers」第 59 段(text/13-ch03-3-numbers.txt:59,搜「shapes everything」):「这个单一决定塑造了一切:从金融计算差一分钱,到数过 9,007,199,254,740,991 不可靠,再到 V8 偷偷区别对待小整数。」 ↩

  64. 出处:「3 Numbers」第 65 段(text/13-ch03-3-numbers.txt:65,搜「accurately add 0.1 and 0.2」)。 ↩

  65. 出处:「3 Numbers」第 38 段(text/13-ch03-3-numbers.txt:38,搜「equally deceptive simplicity」):「字符串藏着自己的复杂度,数字也是一种同样具有欺骗性的简单。」对称结构是我们的提炼。 ↩

  66. 出处:「3 Numbers」第 763–995 段的完整推演链(text/13-ch03-3-numbers.txt:828,搜「repeating pattern 1001」;:879 搜「Every language using IEEE 754」)。五步版本是我们的压缩。 ↩

  67. 出处:「3 Numbers」第 879 段(text/13-ch03-3-numbers.txt:879,搜「from Python to Java to C++」):「Every language using IEEE 754 floating-point has this same behavior, from Python to Java to C++.」 ↩

  68. 出处:「3 Numbers」第 711–746 段与第 796–995 段(text/13-ch03-3-numbers.txt:802,搜「1.1001100110011」)。 ↩

  69. 出处:「3 Numbers」第 1114–1122 段(text/13-ch03-3-numbers.txt:1114,搜「console.log(9007199254740992)」)。 ↩

  70. 出处:「3 Numbers」第 122 段(text/13-ch03-3-numbers.txt:122,搜「not going to cover the full detail」)与第 633 段(搜「deep details of IEEE-754」)。 ↩

On this page