V8 Number类压缩
之前知道V8对堆地址和小整数(Smi)即Number类进行了压缩,只保存了低32bit的地址,但调试时就会发现高位地址并非全0,便好奇高位地址的作用是什么。
Smi就是指针是合法的整形。
arduino
class Smi: public Object {
public:
inline int32_t value(){
return static_cast<int32_t>(reinterpret_cast<intptr_t>(this)) >> 1;
}
static Smi* fromInt(int32_t value){
intptr_t tagged_value = (static_cast<intptr_t>(value) << 1) | 0;
return reinterpret_cast<Smi*>(tagged_value);
}
static bool isSmi(Smi* ptr){
return (reinterpret_cast<intptr_t>(ptr) & 1) == 0;
}
};
原因
任何独特的操作都有其出现的原因。
1. 64位系统的内存利用率问题
-
在64位系统中,原生指针占用8字节(64位),而JavaScript对象(如对象头、属性指针等)大量使用指针,导致内存浪费。
- 有一个生活中比较能理解的例子,用过edge的朋友都知道,网页一开多内存的占用会达到非常夸张的巨大程度。
2. 堆内存范围的限制
-
V8将堆内存限制在 4GB或不到的连续虚拟地址空间 内。
- 4GB的地址空间仅需32位((2^{32} = 4,294,967,296) 字节)即可覆盖,因此低32位足以表示堆内任何对象的偏移量。
3. 高32位的固定基址设计
- 基址的高位固定 : V8在初始化堆时,分配一块连续内存区域,并确保其起始地址(基址)的高32位是固定的(如
0x00001234
)。
4. 压缩的收益
- 内存节省: 将指针从8字节压缩为4字节,显著减少JavaScript对象的内存占用(如对象头、隐藏类、属性指针等),降低GC压力。
- 性能提升: 更小的内存占用提高了CPU缓存命中率,减少内存带宽消耗,从而加速代码执行。
标记值(Tagged Values)的存储
-
Smi:
- 64位架构下使用32位存储数值,低32位直接保存整数(符号扩展到64位)。
- 标记位为最低位
0
,与指针区分。
-
指针 :
- 低32位存储从基地址开始的偏移量,标记位为
01
(区分强/弱引用)。 - 高32位通过基地址隐式确定,无需存储。
- 低32位存储从基地址开始的偏移量,标记位为
所以高位地址的作用是什么?同样是32位内容的存储。
性能优化过程
- 分支优化 :发现有分支解压代码比无分支快7%(依赖CPU分支预测)。
- 消除冗余操作 :在TurboFan编译器中新增"解压消除"阶段,减少不必要的压缩/解压操作。
- 汇编优化 :直接加载并符号扩展32位值,减少指令数。
- 模式匹配修复 :调整优化阶段的匹配规则,恢复分配预配置等关键优化。
- Smi处理改进 :忽略Smi高32位,统一解压逻辑(无条件加基地址)。
回归源代码
v8/src/heap/heap-write-barrier.cc at main · v8/v8
scss
#ifdef V8_COMPRESS_POINTERS // 条件编译:仅在启用指针压缩时生效
// 获取当前线程的标记屏障(Marking Barrier)
// 用于追踪堆中对象的存活状态,确保垃圾回收的正确性
MarkingBarrier* marking_barrier = CurrentMarkingBarrier(host);
// 通过标记屏障获取当前Isolate实例(V8的隔离环境)
// IsolateForPointerCompression 是用于指针压缩的上下文封装
IsolateForPointerCompression isolate(marking_barrier->heap()->isolate());
// 获取C++堆指针表(管理外部C++对象的压缩指针)
CppHeapPointerTable& table = isolate.GetCppHeapPointerTable();
// 获取指针表对应的空间(Space是内存管理的基本单位)
CppHeapPointerTable::Space* space = isolate.GetCppHeapPointerTableSpace();
// 从插槽中以宽松内存顺序加载压缩后的外部指针句柄(32位偏移量)
// Relaxed_LoadHandle() 避免不必要的内存屏障开销
ExternalPointerHandle handle = slot.Relaxed_LoadHandle();
// 标记该外部指针为存活:
// 1. 通过基地址(隐含在Isolate中) + 偏移量(handle)解压指针
// 2. 在垃圾回收中标记对应的C++对象为存活
// 3. slot.address() 提供原始存储地址,用于记录或更新指针
table.Mark(space, handle, slot.address());
#endif // V8_COMPRESS_POINTERS // 结束指针压缩条件编译
包括ForRangeImpl
函数中获取页地址也是通过压缩后的地址获取的。
ini
Tagged_t compressed_page = tagged_value & kPageMask;
效果
让V8拥有64位应用的性能,同时拥有32位的内存占用。