跳转至

05 BigInt、Decimal 与 SMI:逃出浮点的三条路

这一章讲什么

上一章的结论是:Number 是个「十进制小数存不准、大整数装不下」的容器。 这一章讲三条出路。第一条现在就能用:BigInt,任意精度整数, 代价是慢一到两个数量级,而且跟 C 语言家族的定宽整数互相转换时处处是坑。 第二条还在路上:Decimal 类型,语言委员会正在做,十进制编码, 专治金融计算。第三条你一直在用却不知道:V8 的 SMI 优化—— 规范要求所有数字按 64 位浮点对待,但 V8 偷偷把小整数藏在指针里走快车道, 而你有几个写法会亲手把代码撵出这条道。

顶层全景:一个数的三级待遇

  V8 看到一个数字
     │
     ├─ 是 -2^31 ~ 2^31-1 的整数? ──→ SMI:值直接编码在指针里,不占堆、免分配
     │
     ├─ 是更大的整数/小数/NaN/Infinity? ──→ HeapNumber:堆里分配一个对象存 64 位
     │
     └─ 带 n 后缀的? ──→ BigInt:变长的 64 位块数组,要多少给多少

  图说:同一个「42」,走 SMI 通道时 V8 几乎免费;写 42.0 就掉进 HeapNumber,
        每一次加减都要先解一次引用。

核心原理

3.1 BigInt:用内存换正确性

BigInt 是语言里的第二个数字类型,解决的是「整数超过 2⁵³−1 就开始撒谎」的问题。 办法釜底抽薪:不 再固定 64 位,要多少内存给多少,多大整数都能精确表示1。

写法上只差一个后缀 n2:

const unsafe = 9007199254740993;    // Number:存进去就是 9007199254740992,错了
const safe   = 9007199254740993n;   // BigInt:精确,一就是一
const googol = 10n ** 100n;         // 1 后面 100 个 0,Number 想都不敢想

它内部怎么存的? 规范对 BigInt 的实现留了很大自由度;V8 的选择是 64 位块数组。拿 30 位的 123456789012345678901234567890n 来说, 一个 64 位块最大装 18,446,744,073,709,551,615,装不下,就拆成两块: 低 64 位存一块、高 64 位存另一块,符号单独一个标志位3。 重建时把高块左移 64 位再加低块,原值回来4。这个「分块(切成一块一块)+进位」的结构 就是 BigInt 一切性质的来源:能无限加长(只要内存够),但每多一块就多一份 分配和遍历的开销。

性能账原书给了数字:固定宽度的 Binary64 能直接用硬件的算术指令, BigInt 只能在变长数组上用软件实现算术——加法慢 10–20 倍,除法慢 100–200 倍, 而且数越大越慢(块越多)5。所以 BigInt 不是「更大的 Number」, 是另一种东西:正确性换速度。

跟外部系统接口,坑在哪。 BigInt 的任意精度遇上 C 家族的定宽整数 (int64_t、uint64_t),是个著名的麻烦地带6: 语言没有内置的安全转换,你必须自己先验范围——超了就是静默截断或更难看的错; V8 和 Node.js 自己都在这栽过,书里直言「Node.js 内部都出过不少 BigInt 转定宽整数时检查没做对的 bug」7。 作者的示例 toInt64Safe 函数就是手动比对上下界,越界抛 RangeError8。

跟 Number 的相处规则:算术运算禁止混用——10n + 5 直接抛 TypeError,必须显式转换一头9。例外是比较:10n > 5、10n == 10 都合法, 因为比较只产出 true/false,不存在精度损失(值跟真值对不上的那部分)10。10n === 10 则还是 false (类型都不同,下一章细讲 === 的规则)。

什么时候用哪个,书里的判别表可以直接抄:

用 Number 用 BigInt
值在 ±(2⁵³−1) 之内 值会超过安全整数
性能要紧(游戏、图形、实时) 正确性要紧(金融、密码)
百万级次的运算 跟外部系统的 64 位整数对接

11

3.2 Decimal:还没来,但值得知道

钱的问题 Number 和 BigInt 都治不好:前者存不准 0.1,后者根本没有小数。 书里给的例子:19.99 美元按 8.25% 计税,浮点给出 1.6491749999999998, 发票上每一位都是看得见的错12。

正在推进的解法是第三个类型 Decimal,基于 IEEE 754 的 Decimal128 编码 ——注意后缀 128:它是另一种 128 位的容器,但内部用十进制而不是二进制, 跟人算账的方式一致13。提案的写法带 m 后缀:

const price = 19.99m;
const tax = price * 0.0825m;      // 1.649175m,精确
0.1m + 0.2m === 0.3m;             // true,终于

(语法尚未最终定稿,书里标注了「proposed, not yet finalized」14。) Decimal128 与 Binary64 的关键差异:34 位十进制精度(Number 约 17 位)、 十进制值精确表示、为金融计算准备的标准舍入模式、没有二进制转换的意外15。 代价和 BigInt 同方向:运算显著慢于 Number——「但对金融计算,正确性远重于速度」16。

作者的展望是三种类型各司其职的终局:Number 管通用计算,BigInt 管大整数, Decimal 管精确十进制17。这个格局本身说明了语言演进的现实: 数字类型不是设计出来的,是事故之后补出来的——每个类型都对应一类 「上一个类型治不好的生产事故」。

3.3 SMI:V8 给整数的地下快道

回到实现层。规范说每个 Number 都是 64 位浮点,但现实中绝大多数运算是 小整数在加减:循环计数器、数组下标、对象字段(对象里的数据项)计数。 把每个循环变量都当浮点在堆里存一遍,是巨大的浪费18。 V8 的对策是本章第三个机制,也是性能影响最直接的一个:SMI (Small Integer,小整数表示)。

SMI 的做法:值直接塞进指针的位置里。 正常流程是:堆上分配 8 字节存浮点值, 指针记位置,用时先解引用。SMI 把这一步全省了:64 位系统上, −2³¹ 到 2³¹−1(约 ±21 亿)之间的整数,值直接编码在本来放指针的那个槽位(存一个值的位置)里—— 不分配、不解引用,加减就是整型运算19。

超出 SMI 范围呢?V8 分配一个 HeapNumber 对象,64 位浮点值存进堆里, 原来的槽位改存指向它的指针20。V8 内部的决策流程,书里用伪代码写得明明白白21:

function howV8StoresNumber(value) {
  if (Number.isInteger(value)) {
    if (value >= -2147483648 && value <= 2147483647) {
      return "SMI: 直接存,不分配";
    }
    return "HeapNumber: 超 SMI 范围";
  }
  return "HeapNumber: 小数永远走堆";
}

性能悬崖就埋在 2147483647 和 2147483648 之间。 书里有一个对照实验: 同一个循环跑一千万次加减,初始值设成 2147483647(SMI 上限,全程保持 SMI) 和 2147483648(一开始就 是 HeapNumber),后者慢 2–3 倍22。 悬崖的来源不是运算本身变慢,而是每次读写都多一次内存分配和指针解引用; 循环一亿次,这点差就滚成雪球。

主线例子:算账的两种写法。 还是那个税的场景,两种实现:

// 慢版:total 从 0( SMI)开始,第一圈加上小数就变成 HeapNumber,从此一路堆分配
function calculateTotalSlow(prices) {
  let total = 0;
  for (let i = 0; i < prices.length; i++) {
    total += prices[i] * 1.0825;
  }
  return total;
}

// 快版:全程用「分」做整数运算,total 始终是 SMI;最后返回时才除 100 变一次浮点
function calculateTotalFast(pricesInCents) {
  let total = 0;
  for (let i = 0; i < pricesInCents.length; i++) {
    const withTax = Math.round(pricesInCents[i] * 10825 / 10000);
    total += withTax;
  }
  return total / 100;
}

(两个函数都照原书示例23。)快版书里报的结果:至少快一倍24。 顺带发现:这个「用分不用元」的写法同时还是 3.2 节 Decimal 问题的土办法—— 整数运算没有舍入误差,Math.round 之后的值是精确的。

但是,先别激动。 原书在这段特意踩了刹车:SMI 和 HeapNumber 的差距 随引擎版本、CPU、缓存、优化层级而变,「多数时候这个区别无关紧要」; 真要动手,只在「百万级元素的紧循环、内存受限设备、游戏/实时系统」 这三类场景里值得25。作者的总结论值得原文引用级别地记住: 不是「永远用 SMI」,而是「类型混用有代价、一致性帮 V8 优化、 先测量再优化」26。

3.4 让 V8 保持快道的四条写法

书末的四条实践建议,每条都有机制在背后27:

  1. 循环计数器写成整数:for (let i = 0; ...) 里的 i 保持 SMI; 写成 let i = 0.0 会一开始就是 HeapNumber。
  2. 能用整数算术就别用浮点:百分比先乘 100 再除,除法放最后。
  3. 数组里别混数字类型:一个全整数数组里只要混进一个小数, V8 会按最差情况处理——全部元素降级成 HeapNumber28。
  4. 别在热路径上混用 Number 和 BigInt:两条完全不同的表示, 引擎没法为它们同时保持优化假设。

3.1b BigInt 的块账:一个 30 位整数的解剖

把 123456789012345678901234567890n 拆开看,V8 的块数组长这样33:

块 存的值 职责
0 3,584,949,746 低 64 位
1 6,692,605,942 高 64 位

(两行的值均照抄原书表 3.6。)重建时把块 1 左移 64 位—— 相当于十进制里把一组数字整体左移一格再补零——加上块 0, 原值回来;正负号不占块,单独一个标志位34。 这个结构决定了 BigInt 的一切:位数超过 64 就再加块, 只要内存够,整数想多长就多长;但每多一块, 每次运算就要多处理一块、多一次内存分配——慢是结构性的,不是实现偷懒35。

3.1c 跟定宽整数打交道:三个具体的坑

书里列了 BigInt 跨语言接口的三个挑战,个个有实例36:

坑一:没有内置的安全转换。 语言不提供「BigInt → int64/uint64」的 带检查版本,你只能先自己验范围,越界就抛37。书里的 toInt64Safe 就是样板:上下界分别是 −(2⁶³) 和 2⁶³−1,越界抛 RangeError38。

坑二:无符号有歧义。 JavaScript 的 Number 没有「无符号」概念, 可 C 家族的 uint64 有。2n ** 64n - 1n 这个值(uint64 的上限)到底该 转成 uint64 还是因超出 int64 上限而抛错?语言没答案,协议自己定39。

坑三:每次跨界都要查。 每次 BigInt 和原生代码交互都要做范围验证, 这部分开销会吞掉定宽类型本身的性能优势——为了省时间而引入的类型, 在边界上把时间又花了出去40。

作者的结论带着亲历的血泪:这类转换的坑横跨数据库驱动、密码学库, 连 Node.js 自己都栽过——Node 内部就出过不少「BigInt 转定宽整数时 没检查对」的 bug41。用 BigInt 接外部系统的数据, 范围检查不是可选项,是保命项。

3.1d 性能账的另一种说法

10–20 倍(加)和 100–200 倍(除)的差距,书里给了解释: Binary64 的运算直接落在硬件指令上,一条加法指令完事; BigInt 是在变长数组上用软件一步步实现进位—— 位数翻倍,块数翻倍,步数跟着翻倍,越大的 BigInt 越慢42。 所以 BigInt 的正确用法是「精确性必需时才出场」: 金融的总额、密码学的模运算、跟 64 位系统的整数对接; 而不是「图省事的大 Number」。

3.2b Decimal 的账:19.99 元为什么非要新类型

3.2 节说了 Decimal 治什么病,这里把病征的数字摆全。 一条最简单的发票计算43:

const subtotal = 19.99;
const tax = subtotal * 0.0825;   // 8.25% 税
console.log(tax);                // 1.6491749999999998

差多少?1.6491749999999998 对 1.649175,尾数差了 2×10⁻¹⁶—— 写在发票上是「不存在」的一厘钱,可账要对平,这一厘就是全部44。 BigInt 治不了它:19.99 不是整数。Number 治不了它:0.0825 在二进制里 无限循环。病在进制,不在实现——所以药方只能是十进制的容器: IEEE 754 的 Decimal128,128 位宽度,内部按 10 的幂展开45。

提案的形态已经能读:19.99m 字面量、* 0.0825m 乘法、 0.1m + 0.2m === 0.3m 终于是 true46。相对 Binary64, Decimal128 的四项硬指标:34 位十进制精度(Number 约 17 位)、 十进制值精确表示、面向金融计算的标准舍入模式、 没有二进制转换的意外47。代价照旧是性能:Decimal 运算会显著慢于 Number——「但对金融计算,正确性远重于速度」48。

作者给三种类型排了终局座位,这句话值得背: Number 管通用计算,BigInt 管大整数,Decimal 管精确十进制49。 注意这个名单是「事故驱动」排出来的:每个类型都补的是上一个类型 在生产里摔过的坑——语言的类型系统是这么长出来的,不是设计出来的。

3.3b SMI 的机制细节:值怎么「住进」指针

3.3 节说了 SMI 免分配,这里把机制说透。普通值的存法: 堆里分配 8 字节存内容,槽位里放指向它的地址;用时先按地址去堆 里取—— 分配、解引用,两次内存操作50。SMI 的存法:值本身直接编进槽位。 64 位系统上,−2³¹ 到 2³¹−1 的整数享受这个待遇——根本没有什么 「指向 42 的指针」,槽位里就是 42 的编码形式51。 省掉的不只是内存,还有每一次访问的解引用; 整数加减因此退化成纯寄存器操作,这就是「快 10 倍」的来源。

配套的第二层是 HeapNumber:超出 SMI 范围的整数、以及所有小数, 都必须在堆里有一个真身。V8 的判断流程照原书伪代码逐级问三个问题: 是整数吗?在 ±2³¹−1 内吗?不是或不在,一律 HeapNumber52。 注意两个推论:0.0 和 3.14 从出生第一天就是 HeapNumber, 不存在「先 SMI 后升级」;而 2147483648 这个「SMI 上限 + 1」 是第一条分界线,越线即分配。

3.3c 悬崖实验:同一个循环,两个初始值

把性能悬崖变成可复现实验,原书的做法干净利落53:

let value = 2147483647;            // SMI 上限
console.time('SMI arithmetic');
for (let i = 0; i < 10000000; i++) {
  value - 1;                       // 全程保持 SMI
  value + 1;
}
console.timeEnd('SMI arithmetic');

value = 2147483648;                // SMI 上限 + 1,落入 HeapNumber
console.time('HeapNumber arithmetic');
for (let i = 0; i < 10000000; i++) {
  value - 1;                       // 全程 HeapNumber
  value + 1;
}
console.timeEnd('HeapNumber arithmetic');

循环体一千万次,唯一变量是初始值差 1。结果:HeapNumber 版慢 2–3 倍54。 慢的不是减法本身,而是每圈两次的「按指针去堆里取数、算完可能再写回」 ——内存访问比寄存器操作贵一到两个数量级,这里只是小露一角。

3.3d 什么时候才该在乎 SMI

原书在这一段特意泼了冷水,而且泼得很有水平:先列出影响差距的五个变量 ——引擎版本、CPU 架构、数组大小、缓存容量、优化层级—— 然后明说两次运行同一台机器都可能给出不同结果, 所以「本书不打算给出具体的基准数字」55。紧接着是更泼冷水的一句: 对大多数 JavaScript 代码,SMI 与 HeapNumber 的区别无关紧要; 真正值得在乎的只有三类场景——百万级元素的紧循环、内存受限设备、 游戏与实时系统56。最后把元规则说死: 不是「永远用 SMI」,而是「类型混用有代价、一致性帮引擎优化、 先测量再优化」57。这三句的顺序不能反:先测,再改; 一致性比技巧重要。

3.4b 四条建议的逐条机制

3.4 节列了四条,这里给每条配上「为什么会这样」:

  1. 循环计数器写整数:1 是 SMI,1.0 是 HeapNumber—— 数值相同,存储路径完全不同;循环变量一旦带上小数点, V8 只能按最差情况(浮点)优化整个循环58。
  2. 整数算术优先:百分比先乘 100 再除,除法放最后; 中间值留在 SMI 区间,最后一步才容忍一次 HeapNumber59。
  3. 数组不混类型:V8 按数组里出现过的「最差类型」决定整个数组的存储; 一个浮点值混进全整数数组,所有元素都升级为 HeapNumber60。
  4. 不在热路径混 Number/BigInt:两种表示完全不同, 引擎无法为它们维护同一个优化假设61。

四条的本质是一句话:让引擎对「下一个值的形状」有把握。 书里那句「类型一致帮助 V8 优化」不是风格建议,是机制描述62。

3.4c 金融用例的完整账:两种写法的逐圈状态

3.3 节给了快慢两版的代码,这里把对照实验跑全。输入是一百万件单价 19.99 的商品 (演示数据,原书同),两个函数做同一件事「单价含税求和」63:

慢版的每一圈发生了什么: total 从 0(SMI)起步;第一圈 prices[0] * 1.0825 得 21.639…——小数一出现,乘法结果是 HeapNumber, += 之后 total 也沦为 HeapNumber;从此每一圈都带一次堆分配与解引用, 循环变量 i 虽是 SMI,可加法运算的另一操作数是堆里的浮点, 引擎只能按「对象运算」的通用路径走64。

快版的每一圈发生了什么: 输入换成「分」(1999), Math.round(1999 * 10825 / 10000) = 2164——整数进、整数出, 全程 SMI;累加一百万次后 total 约 2.16×10⁹,仍在 SMI 上限 21.4 亿附近 徘徊(演示数据下未越线;真越了也只是一次性降级)65; return total / 100 把整数和一次性转成元,舍入发生在最后一次66。

结果:快版「至少快一倍」67。更妙的是它的副作用: 整数的累加没有二进制舍入,Math.round 之后每个中间值都精确—— 同一个写法同时解决了 04 章的精度问题和本章的性能问题。 「用分不用元」因此是 JavaScript 财务代码的祖传偏方: 不是 hack,是把两个机制(SMI 快道 + 整数精确)一次用满。

3.4d 那五条影响变量的实际含义

「差距随机器而变」听着像推卸,书里给了具体的五个变量68, 翻译过来每个都有明确的机制:

变量 为什么影响 SMI/HeapNumber 差距
引擎版本 V8 的优化器(负责提速的内部部件)逐版本重写,同样的代码可能走不同的优化路径
CPU 架构 内存层级(缓存、主存)不同,解引用的代价不同
数组大小 数据量决定「每圈贵一点」是否滚成雪球
缓存容量 堆对象是否常驻缓存,直接决定解引用成本
优化层级 代码跑在解释器、基线编译还是优化编译,待遇天差地别

所以正确的工程姿势被作者定成三句话: 该用哪个类型用哪个(大整数用 BigInt、通用数学用 Number)、 性能关键路径上保持类型一致、只在测出真问题后才优化。69

3.5b 把两条线索收成一张对照表

走完第 3 章,值得把「什么时候用什么」收成一张能贴在工位上的表:

场景 用什么 为什么
通用计算、动画、图形 Number 硬件加速,误差无人当真
值可能超 2⁵³−1 的整数 BigInt 精确,慢 10–200 倍但正确
跟数据库/原生代码的 64 位整数对接 字符串或 BigInt + 手动验范围 静默改号比慢更可怕
钱 整数算分 + 最后一步转换(或等 Decimal) 同时绕开舍入误差和 SMI 降级
百万级元素的紧循环 全程整数,数组不混类型 保住 SMI 快道与形状稳定

这张表的第 2、4、5 行都是「一个机制对应一条建议」的实例: BigInt 的变长块、SMI 的 ±2³¹、V8 的按最差类型存数组—— 先懂机制,表才是活的;不懂机制,表只是咒语。 这一章(连同 04) 的全部内容,就是把这些机制各讲一遍。

3.5c 原书第 3 章的章末总结:十二条核对单

原书章末给了十二条总结,几乎每一条都在本章有一节与之对应, 照录如下,当作读完的核对单70:

  1. Number 是 64 位浮点(Binary64),无法精确表示 0.1、0.2 这类十进制小数;
  2. 二进制表示让 0.1+0.2 等于 0.30000000000000004——无限循环模式的宿命;
  3. 补码让带符号整数「取反加一」,同一套电路处理正负;
  4. 浮点用范围换精度:53 位准确度,从 5e-324 到 1.8e308;
  5. 最大安全整数 2⁵³−1,之上连整数都不能精确;
  6. Number.EPSILON 是两个可表示数之间的最小差,用来判「差不多相等」;
  7. BigInt 用动态内存换任意精度的整数,代价是慢 10–200 倍;
  8. 算术禁止 Number 与 BigInt 混用(比较除外);
  9. 提案中的 Decimal 用十进制编码,服务金融与科学计算;
  10. V8 把 −2³¹ 到 2³¹−1 的整数当 SMI,免堆分配;
  11. 数组与循环里类型一致,帮 V8 保持优化;混类型则全部降级;
  12. 「什么时候用哪个」:Number 通用、BigInt 大整数、Decimal(将来)精确十进制。

这十二条里没有一条是「语法知识」,全是「机制知识」—— 这正是这本书和所有入门教程的分界线:语法教你「怎么写」, 机制教你「写出来会发生什么」。

如果只能带走一句,我们选第 12 条的原因:「什么时候用哪个」 是所有「数字怪问题」的归宿——0.1+0.2 不是怪问题,是「用错了容器」; uint64 ID 变样不是怪问题,是「边界上没人验范围」;排序慢 40 倍不是怪问题, 是「类型在循环里反复换容器」。容器选对了,JavaScript 的数字就没有怪问题。

3.1e 什么时候用 BigInt:原书的两张清单

3.1 节给了压缩版判别表,原书其实是两张完整清单,值得照录71:

用 Number,当:

  • 值待在 ±(2⁵³−1) 以内;
  • 性能要紧——游戏、图形、实时处理;
  • 要做百万级次的运算。

用 BigInt,当:

  • 正确性比速度重要——金融、密码学;
  • 值会超过最大安全整数;
  • 在跟其他系统的 64 位整数打交道。

两张清单的共同底层是本章反复出现的那杆秤:速度和正确性,你总要选一个, 而「选」的前提是知道两种表示的真实代价。 不知道 SMI/HeapNumber 之分的人, 会把「Number 快」理解成「Number 总是对的」;不知道块数组的人, 会把「BigInt 慢」理解成「BigInt 没用」——两张清单是纠这两种误解的。

3.5d 一张「容器选择流程图」(文字版)

遇到「这个数该怎么存」的问题,按顺序问四个问题72:

  这个数有小数部分吗?
   ├─ 没有 → 值会超过 9,007,199,254,740,991 吗?
   │          ├─ 不会 → Number(留在 SMI 区间更快)
   │          └─ 会 → BigInt,跨界时手动验范围
   └─ 有 → 误差有人当真吗(钱、发票、对账)?
              ├─ 不算 → Number(注意 EPSILON 判等)
              └─ 算 → 整数化(用分算)等 Decimal,别赌浮点

  图说:四个问题,两次「会不会/算不算」的分叉;
        每一次分叉背后,都是本章讲过的一个机制。

这张图没有新内容,只是把 04、05 两章的机制折成一条决策路径。 值得点明的是最后那个「别赌浮点」:它不是对浮点的否定, 而是对「误差有没有人当真」这个问题的尊重—— 同一台计算器,在物理实验室里是神器,在收银台里是事故,区别只在观众。

作者的判断与证据

有证据的判断:

  • BigInt 的 10–20 倍/100–200 倍差距、SMI 悬崖的 2–3 倍差距,来自书中实验, 但作者对 SMI 部分明确拒绝给出具体数字,理由是变量太多29—— 诚实的做法,值得效仿:看方向,别抄数字。
  • 「Node.js 自己都栽在 BigInt 转换上」是作者以核心维护者身份给出的一手证词30。

作者的主张:

  • 「三种类型各司其职」的终局展望,是方向判断;Decimal 尚在提案阶段, 时间表无人知晓31。
  • 四条写法建议属于性能工程的最佳实践,书里也承认其对普通代码「无关紧要」。

判断(我们的,不是书里的): 本章最该带走的态度是作者对性能数字的拒绝—— 「变量太多,不给了,看方向」比给出漂亮基准更负责任;「先测量、再优化」 在这里不是客套话,而是对 SMI 这类实现层细节的诚实定性。 如果错,会错在: 在嵌入式或游戏这类性能预算极紧的场景里,「方向正确」可能不够, 必须拿到具体数字——那时本章的克制就成了缺口,需要去读 V8 的专项 benchmark 而不是等书给答案。

边界与局限

  • Decimal 的一切(语法 19.99m、128 位编码、34 位精度)都以提案为准, 落地时可能变;书里也标了「尚未最终定稿」32。
  • SMI 的范围(±2³¹)是 V8 在 64 位系统上的实现细节,不是规范;其他引擎 (SpiderMonkey、JSC)有类似机制但参数不同。
  • BigInt 与 JSON 的冲突(标准 JSON 没有 BigInt 类型,JSON.stringify(10n) 直接抛错) 原书未展开——这是实际工程里天天踩的点。
  • 「内存受限设备适合 SMI」等建议依赖具体场景,书里没有给内存数字的参照。

可带走的

  1. BigInt = 变长 64 位块数组;加 n 后缀;慢 10–200 倍;跟定宽整数互转要自己验范围。
  2. 10n + 5 抛 TypeError(防混用),10n > 5 合法(比较不丢精度)。
  3. Decimal(提案中)用十进制编码治钱的病:34 位精度、精确表示、标准舍入。
  4. 钱的三条路:用整数算分(Math.round)、用 BigInt、等 Decimal。
  5. SMI:±21 亿内的整数免堆分配;2147483648 是悬崖;小数永远是 HeapNumber。
  6. 「全程整数、最后一步才转浮点」的算账写法,同时解决性能和精度。
  7. 数组混类型,全体降级;循环计数器别写 1.0。
  8. 性能建议的元规则:先测量,再优化;一致性比技巧重要。

原文地图

主题 原书章 原文位置
BigInt 的解题思路 3 Numbers text/13-ch03-3-numbers.txt:1276(搜「abandoning the fixed-width」)
n 后缀与 googol 同上 text/13-ch03-3-numbers.txt:1302(搜「googol」)
64 位块数组实现 同上 text/13-ch03-3-numbers.txt:1329(搜「array of 64-bit chunks」) · :1388(搜「shifted left by 64」)
定宽接口之坑 同上 text/13-ch03-3-numbers.txt:1400(搜「treacherous」) · :1507(搜「failure to correctly check」)
手动验范围 同上 text/13-ch03-3-numbers.txt:1476(搜「toInt64Safe」)
禁止混用、比较例外 同上 text/13-ch03-3-numbers.txt:1519(搜「forbids mixing」) · :1550(搜「comparison exception」)
10–20 倍 / 100–200 倍 同上 text/13-ch03-3-numbers.txt:1556(搜「10-20 times slower」)
Number/BigInt 判别表 同上 text/13-ch03-3-numbers.txt:1568(搜「Use Number when」) · :1591(搜「Correctness matters more than speed」)
Decimal 提案与 19.99m 同上 text/13-ch03-3-numbers.txt:1641(搜「Decimal128」) · :1648(搜「19.99m」)
Decimal128 的四项差异 同上 text/13-ch03-3-numbers.txt:1674(搜「34 digits of precision」)
「正确性远重于速度」 同上 text/13-ch03-3-numbers.txt:1688(搜「correctness far outweighs speed」)
三类型终局 同上 text/13-ch03-3-numbers.txt:1700(搜「Numbers for general computation」)
SMI 定义与范围 同上 text/13-ch03-3-numbers.txt:1735(搜「Small Integer (SMI)」)
HeapNumber 与悬崖 同上 text/13-ch03-3-numbers.txt:1759(搜「performance cliff」)
V8 决策伪代码 同上 text/13-ch03-3-numbers.txt:1771(搜「internal decision process」)
2–3 倍悬崖实测 同上 text/13-ch03-3-numbers.txt:1817(搜「2147483647」) · :1844(搜「2-3x slower」)
算账快慢两版 同上 text/13-ch03-3-numbers.txt:1885(搜「calculateTotalSlow」) · :1913(搜「calculateTotalFast」) · :1936(搜「at least twice as fast」)
「多数时候无关紧要」 同上 text/13-ch03-3-numbers.txt:1984(搜「most of the time, it doesn’t」)
四条写法建议 同上 text/13-ch03-3-numbers.txt:2019(搜「Keep loop counters」) · :2086(搜「Avoid mixing numeric types」)
一个浮点值拖垮全数组 同上 text/13-ch03-3-numbers.txt:2092(搜「Even one floating-point value」)
元规则:一致性、先测量 同上 text/13-ch03-3-numbers.txt:2007(搜「consistency in data types」)
BigInt 块账解剖 同上 text/13-ch03-3-numbers.txt:1341(搜「BigInt storage as 64-bit chunks」)
跨界三坑与 Node 自身 bug 同上 text/13-ch03-3-numbers.txt:1470(搜「no built-in way」) · :1507(搜「bugs within Node.js itself」)
Decimal128 四指标 同上 text/13-ch03-3-numbers.txt:1674(搜「34 digits」)
SMI/HeapNumber 悬崖实验 同上 text/13-ch03-3-numbers.txt:1817(搜「2147483647」) · :1844(搜「2-3x slower」)
金融用例与五条变量 同上 text/13-ch03-3-numbers.txt:1878(搜「prices with tax」) · :1964(搜「engine version」)
四条建议的机制 同上 text/13-ch03-3-numbers.txt:2025(搜「1 and 1.0」) · :2092(搜「Even one floating-point value」)


  1. 出处:「3 Numbers」第 1276 段(text/13-ch03-3-numbers.txt:1276,搜「as much memory as necessary」)。 ↩

  2. 出处:「3 Numbers」第 1288 段(text/13-ch03-3-numbers.txt:1288,搜「n suffix distinguishes」)。 ↩

  3. 出处:「3 Numbers」第 1329 段与表 3.6(text/13-ch03-3-numbers.txt:1329,搜「array of 64-bit chunks」;:1347 搜「Chunk」)。 ↩

  4. 出处:「3 Numbers」第 1388 段(text/13-ch03-3-numbers.txt:1388,搜「shifted left by 64 bits」)。「符号单独存为标志」在同段。 ↩

  5. 出处:「3 Numbers」第 1556 段(text/13-ch03-3-numbers.txt:1556,搜「10-20 times slower」)。 ↩

  6. 出处:「3 Numbers」第 1400 段(text/13-ch03-3-numbers.txt:1400,搜「treacherous」)。「BigInt 只受内存限制、定宽类型有硬上限」在同段。 ↩

  7. 出处:「3 Numbers」第 1507 段(text/13-ch03-3-numbers.txt:1507,搜「a fair number of bugs within Node.js itself」)。 ↩

  8. 出处:「3 Numbers」第 1470 段与第 1476–1483 段(text/13-ch03-3-numbers.txt:1470,搜「no built-in way」;实现在 :1476 搜「toInt64Safe」)。 ↩

  9. 出处:「3 Numbers」第 1519 段(text/13-ch03-3-numbers.txt:1519,搜「forbids mixing BigInt and Number」)。 ↩

  10. 出处:「3 Numbers」第 1550 段(text/13-ch03-3-numbers.txt:1550,搜「The comparison exception」)。 ↩

  11. 出处:「3 Numbers」第 1568–1597 段(text/13-ch03-3-numbers.txt:1568,搜「Use Number when」;:1591 搜「Correctness matters more than speed」)。 ↩

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

  13. 出处:「3 Numbers」第 1641 段(text/13-ch03-3-numbers.txt:1641,搜「Decimal128 representation」)。 ↩

  14. 出处:「3 Numbers」第 1648 段代码的注释(text/13-ch03-3-numbers.txt:1654,搜「not yet finalized」)。 ↩

  15. 出处:「3 Numbers」第 1668–1683 段(text/13-ch03-3-numbers.txt:1674,搜「34 digits of precision」;:1677 搜「exact representation」;:1680 搜「proper rounding」)。 ↩

  16. 出处:「3 Numbers」第 1688 段(text/13-ch03-3-numbers.txt:1688,搜「correctness far outweighs speed」)。 ↩

  17. 出处:「3 Numbers」第 1700–1706 段(text/13-ch03-3-numbers.txt:1700,搜「Numbers for general computation」)。 ↩

  18. 出处:「3 Numbers」第 1717 段(text/13-ch03-3-numbers.txt:1717,搜「most JavaScript arithmetic involves small integers」)。 ↩

  19. 出处:「3 Numbers」第 1735 段(text/13-ch03-3-numbers.txt:1735,搜「store small integers directly」)。「±2³¹ 范围」在同段。 ↩

  20. 出处:「3 Numbers」第 1759 段(text/13-ch03-3-numbers.txt:1759,搜「HeapNumber object」)。 ↩

  21. 出处:「3 Numbers」第 1771–1803 段(text/13-ch03-3-numbers.txt:1776,搜「howV8StoresNumber」)。 ↩

  22. 出处:「3 Numbers」第 1813–1844 段(text/13-ch03-3-numbers.txt:1844,搜「2-3x slower」)。 ↩

  23. 出处:「3 Numbers」第 1885–1927 段(text/13-ch03-3-numbers.txt:1885,搜「calculateTotalSlow」;:1913 搜「calculateTotalFast」)。 ↩

  24. 出处:「3 Numbers」第 1936 段(text/13-ch03-3-numbers.txt:1936,搜「at least twice as fast」)。 ↩

  25. 出处:「3 Numbers」第 1984–2002 段(text/13-ch03-3-numbers.txt:1984,搜「most of the time, it doesn’t」;三类场景在 :1990–:2002)。 ↩

  26. 出处:「3 Numbers」第 2007 段(text/13-ch03-3-numbers.txt:2007,搜「not the lesson here」)。 ↩

  27. 出处:「3 Numbers」第 2019–2098 段(text/13-ch03-3-numbers.txt:2019,搜「Keep loop counters and array indices as integers」;:2053 搜「Always use integer arithmetic」;:2086 搜「Avoid mixing numeric types」)。 ↩

  28. 出处:「3 Numbers」第 2092 段(text/13-ch03-3-numbers.txt:2092,搜「Even one floating-point value」)。 ↩

  29. 出处:「3 Numbers」第 1958 段(text/13-ch03-3-numbers.txt:1958,搜「intentionally not going to show」)。 ↩

  30. 出处:「3 Numbers」第 1507 段(text/13-ch03-3-numbers.txt:1507,搜「get tripped up by these conversions」)。 ↩

  31. 出处:「3 Numbers」第 1694 段(text/13-ch03-3-numbers.txt:1694,搜「remain under discussion」)。 ↩

  32. 出处:「3 Numbers」第 1654 段(text/13-ch03-3-numbers.txt:1654,搜「not yet finalized」)。 ↩

  33. 出处:「3 Numbers」表 3.6(text/13-ch03-3-numbers.txt:1341,搜「BigInt storage as 64-bit chunks」):块 0 值 3,584,949,746、块 1 值 6,692,605,942,两行照抄原书。 ↩

  34. 出处:「3 Numbers」第 1388 段(text/13-ch03-3-numbers.txt:1388,搜「shifted left by 64 bits」):「重建时块 1 左移 64 位加上块 0;符号单独存为一个标志。」 ↩

  35. 出处:「3 Numbers」第 1329 段(text/13-ch03-3-numbers.txt:1329,搜「wide degree of freedom」):规范对 BigInt 实现留有很大自由度,V8 选了 64 位块数组;「越大的 BigInt 越慢」见第 1556 段(搜「The larger the BigInt value」)。 ↩

  36. 出处:「3 Numbers」第 1400–1507 段(text/13-ch03-3-numbers.txt:1400,搜「treacherous」;三个挑战依次展开)。 ↩

  37. 出处:「3 Numbers」第 1470 段(text/13-ch03-3-numbers.txt:1470,搜「no built-in way to safely convert」)。 ↩

  38. 出处:「3 Numbers」第 1476–1483 段(text/13-ch03-3-numbers.txt:1476,搜「toInt64Safe」):上下界 -(2n ** 63n) 与 2n ** 63n - 1n,越界抛 RangeError。 ↩

  39. 出处:「3 Numbers」第 1489–1496 段(text/13-ch03-3-numbers.txt:1495,搜「maxUint64」):2n ** 64n - 1n 该转 uint64 还是因超 int64 上限而报错,语言没有答案。 ↩

  40. 出处:「3 Numbers」第 1502 段(text/13-ch03-3-numbers.txt:1502,搜「defeats the purpose」):「每次与原生代码交互都要范围验证,这部分开销抵消了定宽类型本身的高效。」 ↩

  41. 出处:「3 Numbers」第 1507 段(text/13-ch03-3-numbers.txt:1507,搜「a fair number of bugs within Node.js itself」)。原话即如此。 ↩

  42. 出处:「3 Numbers」第 1556 段(text/13-ch03-3-numbers.txt:1556,搜「software-implemented arithmetic」):硬件指令 vs 软件进位。 ↩

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

  44. 出处:「3 Numbers」第 1608–1614 段(text/13-ch03-3-numbers.txt:1614,搜「become infinite repeating patterns」):「Binary64 无法精确表示 0.1、0.2,因为它们在二进制里无限循环,就像 1/3 在十进制里。」 ↩

  45. 出处:「3 Numbers」第 1641 段(text/13-ch03-3-numbers.txt:1641,搜「Decimal128 representation」):「Decimal128 使用十进制而非二进制,与人处理数字的方式一致。」 ↩

  46. 出处:「3 Numbers」第 1648–1658 段(text/13-ch03-3-numbers.txt:1648,搜「19.99m」;:1658 搜「finally」)。 ↩

  47. 出处:「3 Numbers」第 1674–1683 段(text/13-ch03-3-numbers.txt:1674,搜「34 digits」;四项目依次列出)。 ↩

  48. 出处:「3 Numbers」第 1688 段(text/13-ch03-3-numbers.txt:1688,搜「correctness far outweighs speed」)。 ↩

  49. 出处:「3 Numbers」第 1700–1706 段(text/13-ch03-3-numbers.txt:1700,搜「Numbers for general computation」;三种分工依次列出)。 ↩

  50. 出处:「3 Numbers」第 1741 段(text/13-ch03-3-numbers.txt:1741,搜「keeps track of where that memory is located」):普通数值的「分配 + 记地址 + 按址取数」三步。 ↩

  51. 出处:「3 Numbers」第 1735 段(text/13-ch03-3-numbers.txt:1735,搜「store small integers directly」):「V8 可以把小整数直接存在本来放内存地址的位置;64 位系统上 ±2³¹ 享受此待遇。」 ↩

  52. 出处:「3 Numbers」第 1771–1803 段(text/13-ch03-3-numbers.txt:1776,搜「howV8StoresNumber」):先问是否整数,再问是否在 SMI 范围,否则 HeapNumber;「小数永远走堆」见第 1786 段注释。 ↩

  53. 出处:「3 Numbers」第 1813–1844 段(text/13-ch03-3-numbers.txt:1819,搜「console.time('SMI arithmetic')」):两个循环的唯一差别是初始值 2147483647 与 2147483648。 ↩

  54. 出处:「3 Numbers」第 1844 段注释(text/13-ch03-3-numbers.txt:1844,搜「2-3x slower」)。 ↩

  55. 出处:「3 Numbers」第 1958–1979 段(text/13-ch03-3-numbers.txt:1958,搜「intentionally not going to show」):五个影响变量依次列出,作者拒绝给绝对数字。 ↩

  56. 出处:「3 Numbers」第 1984–2002 段(text/13-ch03-3-numbers.txt:1984,搜「most of the time, it doesn’t」):「老实说,大多数时间里它不重要。」三类场景见第 1990–2002 段。 ↩

  57. 出处:「3 Numbers」第 2007 段(text/13-ch03-3-numbers.txt:2007,搜「That’s not the lesson here」):「不是永远用 SMI……类型混用有代价、一致性帮助优化、只在测量出问题后才优化。」 ↩

  58. 出处:「3 Numbers」第 2025–2043 段(text/13-ch03-3-numbers.txt:2025,搜「1 and 1.0」):「1 和 1.0 数值等价,但只有前者能被当 SMI。」两个 for 循环的对照见同段。 ↩

  59. 出处:「3 Numbers」第 2053–2077 段(text/13-ch03-3-numbers.txt:2053,搜「Always use integer arithmetic」):((value * 100) / total) | 0 与浮点版对照。 ↩

  60. 出处:「3 Numbers」第 2092 段(text/13-ch03-3-numbers.txt:2092,搜「Even one floating-point value」):「数组里只要混进一个浮点值,每个元素都会变成 HeapNumber。」 ↩

  61. 出处:「3 Numbers」第 2007 段(text/13-ch03-3-numbers.txt:2007,搜「mixing representations」):「混用表示有代价,哪怕代价很小。」 ↩

  62. 出处:「3 Numbers」第 2007 段(text/13-ch03-3-numbers.txt:2007,搜「consistency in data types helps V8 optimize」)。 ↩

  63. 出处:「3 Numbers」第 1872–1952 段(text/13-ch03-3-numbers.txt:1878,搜「prices with tax」):用例是「处理带税计算的价格数组」;百万件演示数据见第 1943 段(搜「Array(1000000)」)。 ↩

  64. 出处:「3 Numbers」第 1885–1898 段(text/13-ch03-3-numbers.txt:1885,搜「calculateTotalSlow」):「total 从 SMI 开始,第一圈加上小数就变成 HeapNumber。」注释 #B Forces HeapNumber 在同段。 ↩

  65. 出处:「3 Numbers」第 1913–1927 段(text/13-ch03-3-numbers.txt:1913,搜「calculateTotalFast」):「total 始终保持 SMI,只在 return 时转换。」(「约 2.16×10⁹ 未越线」是为演示补的状态推算,数据本身照原书。) ↩

  66. 出处:「3 Numbers」第 1927 段注释(text/13-ch03-3-numbers.txt:1926,搜「Converts to HeapNumber only at the return」)。 ↩

  67. 出处:「3 Numbers」第 1936 段(text/13-ch03-3-numbers.txt:1936,搜「at least twice as fast」)。 ↩

  68. 出处:「3 Numbers」第 1964–1979 段(text/13-ch03-3-numbers.txt:1964,搜「JavaScript engine version」):五个变量依次列出。 ↩

  69. 出处:「3 Numbers」第 2007 段(text/13-ch03-3-numbers.txt:2007,搜「using the appropriate type for the problem」):「合适的类型用在合适的问题上、性能关键路径保持一致、测量出问题后才优化。」 ↩

  70. 出处:「3 Numbers」第 2104–2140 段(text/13-ch03-3-numbers.txt:2110,搜「Binary64」):十二条总结依次列出,编号是我们的整理。 ↩

  71. 出处:「3 Numbers」第 1562–1597 段(text/13-ch03-3-numbers.txt:1568,搜「Use Number when」;:1585 搜「Use BigInts when」)。 ↩

  72. 出处:决策路径是本两章机制(2⁵³−1、SMI、BigInt 块数组、Decimal 提案)的汇总,各节点的依据见 04 §3.4、本节 3.1、3.2、3.3;「误差有没有人当真」的标准见「3 Numbers」第 1052 段(text/13-ch03-3-numbers.txt:1052,搜「not consequential」)。 ↩