Smi 是 V8 中不分配堆内存的小整数,利用地址对齐冗余位作类型标签:32 位系统用 1 位标签、31 位值,范围 −2³⁰~2³⁰−1;64 位系统用 1 位标签、63 位值;算术运算全程无装箱/解箱,溢出时才转 HeapNumber。

小整数(Smi)在 V8 中不是“被优化的对象”,而是从一开始就不走对象分配路径——它根本不在堆上,也不需要 GC、对象头或类型描述结构。这种设计直接源于对硬件地址对齐特性的硬编码利用,是内存与性能双赢的底层工程选择。
为什么 Smi 能绕过堆分配
V8 借用了 CPU 内存对齐的“闲置位”作为类型标签:
- 32 位系统中,所有堆对象地址必为 4 字节对齐,末位恒为 0 → 最低 1 位可复用;V8 规定末位为 0 表示 Smi,为 1 表示 HeapObject 指针
- 64 位系统中,地址按 8 字节对齐,末两位恒为 0 → 最低 2 位空闲;V8 用其中 1 位做 Smi 标签(kSmiTag = 0)
- 因此一个 32 位字长里,Smi 实际使用 31 位存值(含符号位),范围是 −2³⁰ 到 2³⁰−1(−1073741824 ~ 1073741823);64 位下是 63 位有效载荷
Smi 的存储不产生任何堆开销
对比 HeapNumber 就一目了然:
- Smi:纯数值嵌入指针字中,零额外内存 —— 没有 map 字段、没有对象头、不参与 GC 扫描
- HeapNumber:至少占用 12 字节(32 位)或 16 字节(64 位),含 4/8 字节 map + 8 字节双精度值;且因需 GC 管理,还附带标记、移动、扫描等运行时成本
- 像 array.length、for 循环索引 i++、位运算结果 这类高频小整数,全部以 Smi 形式存在,完全规避堆压力
算术运算全程无装箱/解箱
只要参与运算的两个操作数都是 Smi,V8 就直接调用 CPU 整数指令完成:
- a + b、a 、a | b 等操作无需进入堆、不触发类型检查、不生成中间对象
- 仅当结果溢出 Smi 范围(如 Math.pow(2, 31))时,才自动转为 HeapNumber,此时才真正分配堆内存并存储 IEEE-754 值
- 这意味着日常整数计算几乎零抽象损耗,性能逼近原生 C 整数运算
这不是语法糖,而是指针字的双重语义
V8 中每个 JS 值都表示为一个“标记指针(tagged pointer)”,而 Smi 是该指针字的合法解释之一:
- 同一内存位置(比如栈变量或寄存器值),根据末位标签动态决定是解读为整数还是对象地址
- 不需要额外字段、不依赖运行时类型系统判断——标签位本身即类型信息,由硬件保证原子性
- 这种设计让 V8 在保持 JavaScript 动态性的同时,把最常见整数场景的开销压到了理论下限










