05 BigInt、Decimal 与 SMI:逃出浮点的三条路
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 位整数对接 |
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:
- 循环计数器写成整数:
for (let i = 0; ...)里的i保持 SMI; 写成let i = 0.0会一开始就是 HeapNumber。 - 能用整数算术就别用浮点:百分比先乘 100 再除,除法放最后。
- 数组里别混数字类型:一个全整数数组里只要混进一个小数, V8 会按最差情况处理——全部元素降级成 HeapNumber28。
- 别在热路径上混用 Number 和 BigInt:两条完全不同的表示, 引擎没法为它们同时保持优化假设。
3.1b BigInt 的块账:一个 30 位整数的解剖
把 123456789012345678901234567890n 拆开看,V8 的块数组长这样29:
| 块 | 存的值 | 职责 |
|---|---|---|
| 0 | 3,584,949,746 | 低 64 位 |
| 1 | 6,692,605,942 | 高 64 位 |
(两行的值均照抄原书表 3.6。)重建时把块 1 左移 64 位—— 相当于十进制里把一组数字整体左移一格再补零——加上块 0, 原值回来;正负号不占块,单独一个标志位30。 这个结构决定了 BigInt 的一切:位数超过 64 就再加块, 只要内存够,整数想多长就多长;但每多一块, 每次运算就要多处理一块、多一次内存分配——慢是结构性的,不是实现偷懒31。
3.1c 跟定宽整数打交道:三个具体的坑
书里列了 BigInt 跨语言接口的三个挑战,个个有实例32:
坑一:没有内置的安全转换。 语言不提供「BigInt → int64/uint64」的
带检查版本,你只能先自己验范围,越界就抛33。书里的 toInt64Safe
就是样板:上下界分别是 −(2⁶³) 和 2⁶³−1,越界抛 RangeError34。
坑二:无符号有歧义。 JavaScript 的 Number 没有「无符号」概念,
可 C 家族的 uint64 有。2n ** 64n - 1n 这个值(uint64 的上限)到底该
转成 uint64 还是因超出 int64 上限而抛错?语言没答案,协议自己定35。
坑三:每次跨界都要查。 每次 BigInt 和原生代码交互都要做范围验证, 这部分开销会吞掉定宽类型本身的性能优势——为了省时间而引入的类型, 在边界上把时间又花了出去36。
作者的结论带着亲历的血泪:这类转换的坑横跨数据库驱动、密码学库, 连 Node.js 自己都栽过——Node 内部就出过不少「BigInt 转定宽整数时 没检查对」的 bug37。用 BigInt 接外部系统的数据, 范围检查不是可选项,是保命项。
3.1d 性能账的另一种说法
10–20 倍(加)和 100–200 倍(除)的差距,书里给了解释: Binary64 的运算直接落在硬件指令上,一条加法指令完事; BigInt 是在变长数组上用软件一步步实现进位—— 位数翻倍,块数翻倍,步数跟着翻倍,越大的 BigInt 越慢38。 所以 BigInt 的正确用法是「精确性必需时才出场」: 金融的总额、密码学的模运算、跟 64 位系统的整数对接; 而不是「图省事的大 Number」。
3.2b Decimal 的账:19.99 元为什么非要新类型
3.2 节说了 Decimal 治什么病,这里把病征的数字摆全。 一条最简单的发票计算39:
const subtotal = 19.99;
const tax = subtotal * 0.0825; // 8.25% 税
console.log(tax); // 1.6491749999999998差多少?1.6491749999999998 对 1.649175,尾数差了 2×10⁻¹⁶——
写在发票上是「不存在」的一厘钱,可账要对平,这一厘就是全部40。
BigInt 治不了它:19.99 不是整数。Number 治不了它:0.0825 在二进制里
无限循环。病在进制,不在实现——所以药方只能是十进制的容器:
IEEE 754 的 Decimal128,128 位宽度,内部按 10 的幂展开41。
提案的形态已经能读:19.99m 字面量、* 0.0825m 乘法、
0.1m + 0.2m === 0.3m 终于是 true42。相对 Binary64,
Decimal128 的四项硬指标:34 位十进制精度(Number 约 17 位)、
十进制值精确表示、面向金融计算的标准舍入模式、
没有二进制转换的意外43。代价照旧是性能:Decimal 运算会显著慢于
Number——「但对金融计算,正确性远重于速度」44。
作者给三种类型排了终局座位,这句话值得背: Number 管通用计算,BigInt 管大整数,Decimal 管精确十进制45。 注意这个名单是「事故驱动」排出来的:每个类型都补的是上一个类型 在生产里摔过的坑——语言的类型系统是这么长出来的,不是设计出来的。
3.3b SMI 的机制细节:值怎么「住进」指针
3.3 节说了 SMI 免分配,这里把机制说透。普通值的存法: 堆里分配 8 字节存内容,槽位里放指向它的地址;用时先按地址去堆 里取—— 分配、解引用,两次内存操作46。SMI 的存法:值本身直接编进槽位。 64 位系统上,−2³¹ 到 2³¹−1 的整数享受这个待遇——根本没有什么 「指向 42 的指针」,槽位里就是 42 的编码形式47。 省掉的不只是内存,还有每一次访问的解引用; 整数加减因此退化成纯寄存器操作,这就是「快 10 倍」的来源。
配套的第二层是 HeapNumber:超出 SMI 范围的整数、以及所有小数,
都必须在堆里有一个真身。V8 的判断流程照原书伪代码逐级问三个问题:
是整数吗?在 ±2³¹−1 内吗?不是或不在,一律 HeapNumber48。
注意两个推论:0.0 和 3.14 从出生第一天就是 HeapNumber,
不存在「先 SMI 后升级」;而 2147483648 这个「SMI 上限 + 1」
是第一条分界线,越线即分配。
3.3c 悬崖实验:同一个循环,两个初始值
把性能悬崖变成可复现实验,原书的做法干净利落49:
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 倍50。 慢的不是减法本身,而是每圈两次的「按指针去堆里取数、算完可能再写回」 ——内存访问比寄存器操作贵一到两个数量级,这里只是小露一角。
3.3d 什么时候才该在乎 SMI
原书在这一段特意泼了冷水,而且泼得很有水平:先列出影响差距的五个变量 ——引擎版本、CPU 架构、数组大小、缓存容量、优化层级—— 然后明说两次运行同一台机器都可能给出不同结果, 所以「本书不打算给出具体的基准数字」51。紧接着是更泼冷水的一句: 对大多数 JavaScript 代码,SMI 与 HeapNumber 的区别无关紧要; 真正值得在乎的只有三类场景——百万级元素的紧循环、内存受限设备、 游戏与实时系统52。最后把元规则说死: 不是「永远用 SMI」,而是**「类型混用有代价、一致性帮引擎优化、 先测量再优化」**53。这三句的顺序不能反:先测,再改; 一致性比技巧重要。
3.4b 四条建议的逐条机制
3.4 节列了四条,这里给每条配上「为什么会这样」:
- 循环计数器写整数:
1是 SMI,1.0是 HeapNumber—— 数值相同,存储路径完全不同;循环变量一旦带上小数点, V8 只能按最差情况(浮点)优化整个循环54。 - 整数算术优先:百分比先乘 100 再除,除法放最后; 中间值留在 SMI 区间,最后一步才容忍一次 HeapNumber55。
- 数组不混类型:V8 按数组里出现过的「最差类型」决定整个数组的存储; 一个浮点值混进全整数数组,所有元素都升级为 HeapNumber56。
- 不在热路径混 Number/BigInt:两种表示完全不同, 引擎无法为它们维护同一个优化假设57。
四条的本质是一句话:让引擎对「下一个值的形状」有把握。 书里那句「类型一致帮助 V8 优化」不是风格建议,是机制描述58。
3.4c 金融用例的完整账:两种写法的逐圈状态
3.3 节给了快慢两版的代码,这里把对照实验跑全。输入是一百万件单价 19.99 的商品 (演示数据,原书同),两个函数做同一件事「单价含税求和」59:
慢版的每一圈发生了什么: total 从 0(SMI)起步;第一圈
prices[0] * 1.0825 得 21.639…——小数一出现,乘法结果是 HeapNumber,
+= 之后 total 也沦为 HeapNumber;从此每一圈都带一次堆分配与解引用,
循环变量 i 虽是 SMI,可加法运算的另一操作数是堆里的浮点,
引擎只能按「对象运算」的通用路径走60。
快版的每一圈发生了什么: 输入换成「分」(1999),
Math.round(1999 * 10825 / 10000) = 2164——整数进、整数出,
全程 SMI;累加一百万次后 total 约 2.16×10⁹,仍在 SMI 上限 21.4 亿附近
徘徊(演示数据下未越线;真越了也只是一次性降级)61;
return total / 100 把整数和一次性转成元,舍入发生在最后一次62。
结果:快版「至少快一倍」63。更妙的是它的副作用:
整数的累加没有二进制舍入,Math.round 之后每个中间值都精确——
同一个写法同时解决了 04 章的精度问题和本章的性能问题。
「用分不用元」因此是 JavaScript 财务代码的祖传偏方:
不是 hack,是把两个机制(SMI 快道 + 整数精确)一次用满。
3.4d 那五条影响变量的实际含义
「差距随机器而变」听着像推卸,书里给了具体的五个变量64, 翻译过来每个都有明确的机制:
| 变量 | 为什么影响 SMI/HeapNumber 差距 |
|---|---|
| 引擎版本 | V8 的优化器(负责提速的内部部件)逐版本重写,同样的代码可能走不同的优化路径 |
| CPU 架构 | 内存层级(缓存、主存)不同,解引用的代价不同 |
| 数组大小 | 数据量决定「每圈贵一点」是否滚成雪球 |
| 缓存容量 | 堆对象是否常驻缓存,直接决定解引用成本 |
| 优化层级 | 代码跑在解释器、基线编译还是优化编译,待遇天差地别 |
所以正确的工程姿势被作者定成三句话: 该用哪个类型用哪个(大整数用 BigInt、通用数学用 Number)、 性能关键路径上保持类型一致、只在测出真问题后才优化。65
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 章的章末总结:十二条核对单
原书章末给了十二条总结,几乎每一条都在本章有一节与之对应, 照录如下,当作读完的核对单66:
- Number 是 64 位浮点(Binary64),无法精确表示 0.1、0.2 这类十进制小数;
- 二进制表示让 0.1+0.2 等于 0.30000000000000004——无限循环模式的宿命;
- 补码让带符号整数「取反加一」,同一套电路处理正负;
- 浮点用范围换精度:53 位准确度,从 5e-324 到 1.8e308;
- 最大安全整数 2⁵³−1,之上连整数都不能精确;
- Number.EPSILON 是两个可表示数之间的最小差,用来判「差不多相等」;
- BigInt 用动态内存换任意精度的整数,代价是慢 10–200 倍;
- 算术禁止 Number 与 BigInt 混用(比较除外);
- 提案中的 Decimal 用十进制编码,服务金融与科学计算;
- V8 把 −2³¹ 到 2³¹−1 的整数当 SMI,免堆分配;
- 数组与循环里类型一致,帮 V8 保持优化;混类型则全部降级;
- 「什么时候用哪个」:Number 通用、BigInt 大整数、Decimal(将来)精确十进制。
这十二条里没有一条是「语法知识」,全是「机制知识」—— 这正是这本书和所有入门教程的分界线:语法教你「怎么写」, 机制教你「写出来会发生什么」。
如果只能带走一句,我们选第 12 条的原因:「什么时候用哪个」 是所有「数字怪问题」的归宿——0.1+0.2 不是怪问题,是「用错了容器」; uint64 ID 变样不是怪问题,是「边界上没人验范围」;排序慢 40 倍不是怪问题, 是「类型在循环里反复换容器」。容器选对了,JavaScript 的数字就没有怪问题。
3.1e 什么时候用 BigInt:原书的两张清单
3.1 节给了压缩版判别表,原书其实是两张完整清单,值得照录67:
用 Number,当:
- 值待在 ±(2⁵³−1) 以内;
- 性能要紧——游戏、图形、实时处理;
- 要做百万级次的运算。
用 BigInt,当:
- 正确性比速度重要——金融、密码学;
- 值会超过最大安全整数;
- 在跟其他系统的 64 位整数打交道。
两张清单的共同底层是本章反复出现的那杆秤:速度和正确性,你总要选一个, 而「选」的前提是知道两种表示的真实代价。 不知道 SMI/HeapNumber 之分的人, 会把「Number 快」理解成「Number 总是对的」;不知道块数组的人, 会把「BigInt 慢」理解成「BigInt 没用」——两张清单是纠这两种误解的。
3.5d 一张「容器选择流程图」(文字版)
遇到「这个数该怎么存」的问题,按顺序问四个问题68:
这个数有小数部分吗?
├─ 没有 → 值会超过 9,007,199,254,740,991 吗?
│ ├─ 不会 → Number(留在 SMI 区间更快)
│ └─ 会 → BigInt,跨界时手动验范围
└─ 有 → 误差有人当真吗(钱、发票、对账)?
├─ 不算 → Number(注意 EPSILON 判等)
└─ 算 → 整数化(用分算)等 Decimal,别赌浮点
图说:四个问题,两次「会不会/算不算」的分叉;
每一次分叉背后,都是本章讲过的一个机制。这张图没有新内容,只是把 04、05 两章的机制折成一条决策路径。 值得点明的是最后那个「别赌浮点」:它不是对浮点的否定, 而是对「误差有没有人当真」这个问题的尊重—— 同一台计算器,在物理实验室里是神器,在收银台里是事故,区别只在观众。
作者的判断与证据
有证据的判断:
- BigInt 的 10–20 倍/100–200 倍差距、SMI 悬崖的 2–3 倍差距,来自书中实验, 但作者对 SMI 部分明确拒绝给出具体数字,理由是变量太多69—— 诚实的做法,值得效仿:看方向,别抄数字。
- 「Node.js 自己都栽在 BigInt 转换上」是作者以核心维护者身份给出的一手证词70。
作者的主张:
- 「三种类型各司其职」的终局展望,是方向判断;Decimal 尚在提案阶段, 时间表无人知晓71。
- 四条写法建议属于性能工程的最佳实践,书里也承认其对普通代码「无关紧要」。
判断(我们的,不是书里的): 本章最该带走的态度是作者对性能数字的拒绝—— 「变量太多,不给了,看方向」比给出漂亮基准更负责任;「先测量、再优化」 在这里不是客套话,而是对 SMI 这类实现层细节的诚实定性。 如果错,会错在: 在嵌入式或游戏这类性能预算极紧的场景里,「方向正确」可能不够, 必须拿到具体数字——那时本章的克制就成了缺口,需要去读 V8 的专项 benchmark 而不是等书给答案。
边界与局限
- Decimal 的一切(语法
19.99m、128 位编码、34 位精度)都以提案为准, 落地时可能变;书里也标了「尚未最终定稿」72。 - SMI 的范围(±2³¹)是 V8 在 64 位系统上的实现细节,不是规范;其他引擎 (SpiderMonkey、JSC)有类似机制但参数不同。
- BigInt 与 JSON 的冲突(标准 JSON 没有 BigInt 类型,
JSON.stringify(10n)直接抛错) 原书未展开——这是实际工程里天天踩的点。 - 「内存受限设备适合 SMI」等建议依赖具体场景,书里没有给内存数字的参照。
可带走的
- BigInt = 变长 64 位块数组;加 n 后缀;慢 10–200 倍;跟定宽整数互转要自己验范围。
10n + 5抛 TypeError(防混用),10n > 5合法(比较不丢精度)。- Decimal(提案中)用十进制编码治钱的病:34 位精度、精确表示、标准舍入。
- 钱的三条路:用整数算分(Math.round)、用 BigInt、等 Decimal。
- SMI:±21 亿内的整数免堆分配;2147483648 是悬崖;小数永远是 HeapNumber。
- 「全程整数、最后一步才转浮点」的算账写法,同时解决性能和精度。
- 数组混类型,全体降级;循环计数器别写 1.0。
- 性能建议的元规则:先测量,再优化;一致性比技巧重要。
原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 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」) |
Footnotes
-
出处:「3 Numbers」第 1276 段(
text/13-ch03-3-numbers.txt:1276,搜「as much memory as necessary」)。 ↩ -
出处:「3 Numbers」第 1288 段(
text/13-ch03-3-numbers.txt:1288,搜「n suffix distinguishes」)。 ↩ -
出处:「3 Numbers」第 1329 段与表 3.6(
text/13-ch03-3-numbers.txt:1329,搜「array of 64-bit chunks」;:1347搜「Chunk」)。 ↩ -
出处:「3 Numbers」第 1388 段(
text/13-ch03-3-numbers.txt:1388,搜「shifted left by 64 bits」)。「符号单独存为标志」在同段。 ↩ -
出处:「3 Numbers」第 1556 段(
text/13-ch03-3-numbers.txt:1556,搜「10-20 times slower」)。 ↩ -
出处:「3 Numbers」第 1400 段(
text/13-ch03-3-numbers.txt:1400,搜「treacherous」)。「BigInt 只受内存限制、定宽类型有硬上限」在同段。 ↩ -
出处:「3 Numbers」第 1507 段(
text/13-ch03-3-numbers.txt:1507,搜「a fair number of bugs within Node.js itself」)。 ↩ -
出处:「3 Numbers」第 1470 段与第 1476–1483 段(
text/13-ch03-3-numbers.txt:1470,搜「no built-in way」;实现在:1476搜「toInt64Safe」)。 ↩ -
出处:「3 Numbers」第 1519 段(
text/13-ch03-3-numbers.txt:1519,搜「forbids mixing BigInt and Number」)。 ↩ -
出处:「3 Numbers」第 1550 段(
text/13-ch03-3-numbers.txt:1550,搜「The comparison exception」)。 ↩ -
出处:「3 Numbers」第 1568–1597 段(
text/13-ch03-3-numbers.txt:1568,搜「Use Number when」;:1591搜「Correctness matters more than speed」)。 ↩ -
出处:「3 Numbers」第 1627–1631 段(
text/13-ch03-3-numbers.txt:1631,搜「1.6491749999999998」)。 ↩ -
出处:「3 Numbers」第 1641 段(
text/13-ch03-3-numbers.txt:1641,搜「Decimal128 representation」)。 ↩ -
出处:「3 Numbers」第 1648 段代码的注释(
text/13-ch03-3-numbers.txt:1654,搜「not yet finalized」)。 ↩ -
出处:「3 Numbers」第 1668–1683 段(
text/13-ch03-3-numbers.txt:1674,搜「34 digits of precision」;:1677搜「exact representation」;:1680搜「proper rounding」)。 ↩ -
出处:「3 Numbers」第 1688 段(
text/13-ch03-3-numbers.txt:1688,搜「correctness far outweighs speed」)。 ↩ -
出处:「3 Numbers」第 1700–1706 段(
text/13-ch03-3-numbers.txt:1700,搜「Numbers for general computation」)。 ↩ -
出处:「3 Numbers」第 1717 段(
text/13-ch03-3-numbers.txt:1717,搜「most JavaScript arithmetic involves small integers」)。 ↩ -
出处:「3 Numbers」第 1735 段(
text/13-ch03-3-numbers.txt:1735,搜「store small integers directly」)。「±2³¹ 范围」在同段。 ↩ -
出处:「3 Numbers」第 1759 段(
text/13-ch03-3-numbers.txt:1759,搜「HeapNumber object」)。 ↩ -
出处:「3 Numbers」第 1771–1803 段(
text/13-ch03-3-numbers.txt:1776,搜「howV8StoresNumber」)。 ↩ -
出处:「3 Numbers」第 1813–1844 段(
text/13-ch03-3-numbers.txt:1844,搜「2-3x slower」)。 ↩ -
出处:「3 Numbers」第 1885–1927 段(
text/13-ch03-3-numbers.txt:1885,搜「calculateTotalSlow」;:1913搜「calculateTotalFast」)。 ↩ -
出处:「3 Numbers」第 1936 段(
text/13-ch03-3-numbers.txt:1936,搜「at least twice as fast」)。 ↩ -
出处:「3 Numbers」第 1984–2002 段(
text/13-ch03-3-numbers.txt:1984,搜「most of the time, it doesn’t」;三类场景在:1990–:2002)。 ↩ -
出处:「3 Numbers」第 2007 段(
text/13-ch03-3-numbers.txt:2007,搜「not the lesson here」)。 ↩ -
出处:「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」)。 ↩ -
出处:「3 Numbers」第 2092 段(
text/13-ch03-3-numbers.txt:2092,搜「Even one floating-point value」)。 ↩ -
出处:「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,两行照抄原书。 ↩ -
出处:「3 Numbers」第 1388 段(
text/13-ch03-3-numbers.txt:1388,搜「shifted left by 64 bits」):「重建时块 1 左移 64 位加上块 0;符号单独存为一个标志。」 ↩ -
出处:「3 Numbers」第 1329 段(
text/13-ch03-3-numbers.txt:1329,搜「wide degree of freedom」):规范对 BigInt 实现留有很大自由度,V8 选了 64 位块数组;「越大的 BigInt 越慢」见第 1556 段(搜「The larger the BigInt value」)。 ↩ -
出处:「3 Numbers」第 1400–1507 段(
text/13-ch03-3-numbers.txt:1400,搜「treacherous」;三个挑战依次展开)。 ↩ -
出处:「3 Numbers」第 1470 段(
text/13-ch03-3-numbers.txt:1470,搜「no built-in way to safely convert」)。 ↩ -
出处:「3 Numbers」第 1476–1483 段(
text/13-ch03-3-numbers.txt:1476,搜「toInt64Safe」):上下界-(2n ** 63n)与2n ** 63n - 1n,越界抛 RangeError。 ↩ -
出处:「3 Numbers」第 1489–1496 段(
text/13-ch03-3-numbers.txt:1495,搜「maxUint64」):2n ** 64n - 1n该转 uint64 还是因超 int64 上限而报错,语言没有答案。 ↩ -
出处:「3 Numbers」第 1502 段(
text/13-ch03-3-numbers.txt:1502,搜「defeats the purpose」):「每次与原生代码交互都要范围验证,这部分开销抵消了定宽类型本身的高效。」 ↩ -
出处:「3 Numbers」第 1507 段(
text/13-ch03-3-numbers.txt:1507,搜「a fair number of bugs within Node.js itself」)。原话即如此。 ↩ -
出处:「3 Numbers」第 1556 段(
text/13-ch03-3-numbers.txt:1556,搜「software-implemented arithmetic」):硬件指令 vs 软件进位。 ↩ -
出处:「3 Numbers」第 1627–1631 段(
text/13-ch03-3-numbers.txt:1627,搜「19.99」;:1631搜「1.6491749999999998」)。 ↩ -
出处:「3 Numbers」第 1608–1614 段(
text/13-ch03-3-numbers.txt:1614,搜「become infinite repeating patterns」):「Binary64 无法精确表示 0.1、0.2,因为它们在二进制里无限循环,就像 1/3 在十进制里。」 ↩ -
出处:「3 Numbers」第 1641 段(
text/13-ch03-3-numbers.txt:1641,搜「Decimal128 representation」):「Decimal128 使用十进制而非二进制,与人处理数字的方式一致。」 ↩ -
出处:「3 Numbers」第 1648–1658 段(
text/13-ch03-3-numbers.txt:1648,搜「19.99m」;:1658搜「finally」)。 ↩ -
出处:「3 Numbers」第 1674–1683 段(
text/13-ch03-3-numbers.txt:1674,搜「34 digits」;四项目依次列出)。 ↩ -
出处:「3 Numbers」第 1688 段(
text/13-ch03-3-numbers.txt:1688,搜「correctness far outweighs speed」)。 ↩ -
出处:「3 Numbers」第 1700–1706 段(
text/13-ch03-3-numbers.txt:1700,搜「Numbers for general computation」;三种分工依次列出)。 ↩ -
出处:「3 Numbers」第 1741 段(
text/13-ch03-3-numbers.txt:1741,搜「keeps track of where that memory is located」):普通数值的「分配 + 记地址 + 按址取数」三步。 ↩ -
出处:「3 Numbers」第 1735 段(
text/13-ch03-3-numbers.txt:1735,搜「store small integers directly」):「V8 可以把小整数直接存在本来放内存地址的位置;64 位系统上 ±2³¹ 享受此待遇。」 ↩ -
出处:「3 Numbers」第 1771–1803 段(
text/13-ch03-3-numbers.txt:1776,搜「howV8StoresNumber」):先问是否整数,再问是否在 SMI 范围,否则 HeapNumber;「小数永远走堆」见第 1786 段注释。 ↩ -
出处:「3 Numbers」第 1813–1844 段(
text/13-ch03-3-numbers.txt:1819,搜「console.time('SMI arithmetic')」):两个循环的唯一差别是初始值 2147483647 与 2147483648。 ↩ -
出处:「3 Numbers」第 1844 段注释(
text/13-ch03-3-numbers.txt:1844,搜「2-3x slower」)。 ↩ -
出处:「3 Numbers」第 1958–1979 段(
text/13-ch03-3-numbers.txt:1958,搜「intentionally not going to show」):五个影响变量依次列出,作者拒绝给绝对数字。 ↩ -
出处:「3 Numbers」第 1984–2002 段(
text/13-ch03-3-numbers.txt:1984,搜「most of the time, it doesn’t」):「老实说,大多数时间里它不重要。」三类场景见第 1990–2002 段。 ↩ -
出处:「3 Numbers」第 2007 段(
text/13-ch03-3-numbers.txt:2007,搜「That’s not the lesson here」):「不是永远用 SMI……类型混用有代价、一致性帮助优化、只在测量出问题后才优化。」 ↩ -
出处:「3 Numbers」第 2025–2043 段(
text/13-ch03-3-numbers.txt:2025,搜「1 and 1.0」):「1 和 1.0 数值等价,但只有前者能被当 SMI。」两个 for 循环的对照见同段。 ↩ -
出处:「3 Numbers」第 2053–2077 段(
text/13-ch03-3-numbers.txt:2053,搜「Always use integer arithmetic」):((value * 100) / total) | 0与浮点版对照。 ↩ -
出处:「3 Numbers」第 2092 段(
text/13-ch03-3-numbers.txt:2092,搜「Even one floating-point value」):「数组里只要混进一个浮点值,每个元素都会变成 HeapNumber。」 ↩ -
出处:「3 Numbers」第 2007 段(
text/13-ch03-3-numbers.txt:2007,搜「mixing representations」):「混用表示有代价,哪怕代价很小。」 ↩ -
出处:「3 Numbers」第 2007 段(
text/13-ch03-3-numbers.txt:2007,搜「consistency in data types helps V8 optimize」)。 ↩ -
出处:「3 Numbers」第 1872–1952 段(
text/13-ch03-3-numbers.txt:1878,搜「prices with tax」):用例是「处理带税计算的价格数组」;百万件演示数据见第 1943 段(搜「Array(1000000)」)。 ↩ -
出处:「3 Numbers」第 1885–1898 段(
text/13-ch03-3-numbers.txt:1885,搜「calculateTotalSlow」):「total 从 SMI 开始,第一圈加上小数就变成 HeapNumber。」注释#B Forces HeapNumber在同段。 ↩ -
出处:「3 Numbers」第 1913–1927 段(
text/13-ch03-3-numbers.txt:1913,搜「calculateTotalFast」):「total 始终保持 SMI,只在 return 时转换。」(「约 2.16×10⁹ 未越线」是为演示补的状态推算,数据本身照原书。) ↩ -
出处:「3 Numbers」第 1927 段注释(
text/13-ch03-3-numbers.txt:1926,搜「Converts to HeapNumber only at the return」)。 ↩ -
出处:「3 Numbers」第 1936 段(
text/13-ch03-3-numbers.txt:1936,搜「at least twice as fast」)。 ↩ -
出处:「3 Numbers」第 1964–1979 段(
text/13-ch03-3-numbers.txt:1964,搜「JavaScript engine version」):五个变量依次列出。 ↩ -
出处:「3 Numbers」第 2007 段(
text/13-ch03-3-numbers.txt:2007,搜「using the appropriate type for the problem」):「合适的类型用在合适的问题上、性能关键路径保持一致、测量出问题后才优化。」 ↩ -
出处:「3 Numbers」第 2104–2140 段(
text/13-ch03-3-numbers.txt:2110,搜「Binary64」):十二条总结依次列出,编号是我们的整理。 ↩ -
出处:「3 Numbers」第 1562–1597 段(
text/13-ch03-3-numbers.txt:1568,搜「Use Number when」;:1585搜「Use BigInts when」)。 ↩ -
出处:决策路径是本两章机制(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」)。 ↩ -
出处:「3 Numbers」第 1958 段(
text/13-ch03-3-numbers.txt:1958,搜「intentionally not going to show」)。 ↩ -
出处:「3 Numbers」第 1507 段(
text/13-ch03-3-numbers.txt:1507,搜「get tripped up by these conversions」)。 ↩ -
出处:「3 Numbers」第 1694 段(
text/13-ch03-3-numbers.txt:1694,搜「remain under discussion」)。 ↩ -
出处:「3 Numbers」第 1654 段(
text/13-ch03-3-numbers.txt:1654,搜「not yet finalized」)。 ↩