5 年了,V8 引擎发生了什么新变化
5 年了,V8 引擎发生了什么新变化
我在 2021 年写过一篇讲 V8 引擎的文章,覆盖了编译执行流程(Ignition + TurboFan)、deopt、垃圾回收(引用计数、标记清除、分代、增量标记)等。这几年 V8 变化不小——尤其是编译流水线从两层长成了四层。这篇不重复旧文的基础内容,只讲这 5 年发生的新变化,读之前建议先有旧文那些概念打底。
原文链接:聊聊v8引擎
一、最大的变化:编译流水线从两层变成四层
旧架构:两层的断层问题
先回顾旧文的架构。2021 年之前,V8 是经典的两层:
- Ignition:解释器,把 JS 编译成字节码并逐条解释执行。启动快,但执行慢。
- TurboFan:顶级优化编译器,把热点代码 JIT 编译成高度优化的机器码。执行快,但编译慢、成本高。
这套结构有个明显的断层:Ignition 太慢,TurboFan 又太贵。中间缺了过渡档位。现实网页里大量函数是"温热"的——被执行了不少次,值得比解释执行快一点,但又没热到值得花 TurboFan 的重成本去深度优化。旧架构对这些"温热代码"无能为力:要么让它在解释器里慢慢跑,要么砸重金优化它,没有中间选项。
这 5 年,V8 在中间塞进了两个新编译器,把断层填平了。现在完整的四层流水线是:
Ignition → Sparkplug → Maglev → TurboFan (解释器) (基线编译) (快速优化) (顶级优化) 最慢 ↓ ↓ 最快 编译最省 越往右,生成的机器码越快,但编译成本越高代码随着执行次数增加、“越来越热”,就沿着这条流水线逐级向右升级,这个过程叫分层递进优化(tiering up)。下面逐个讲两个新成员。
新成员一:Sparkplug(基线编译器,2021)
Sparkplug 补在 Ignition 之上,它的设计目标只有一个词——快到几乎瞬间地编译。它不做任何复杂优化,只是把字节码近乎一比一地翻译成等价的机器码。
为什么"不优化"也有价值?因为哪怕只是把"解释执行字节码"换成"直接跑机器码",就已经省掉了解释器逐条分发指令的开销。而且因为它编译成本极低,V8 可以大范围地给几乎所有代码都套上这一层,稳定地拿到一档加速。
它的性能收益很实在:在 JetStream 基准上,Sparkplug 相比纯 Ignition 提升约 45%;在更贴近真实网页的 Speedometer 上,相比 Ignition 提升约 41%,甚至相比"Ignition + TurboFan"的旧组合还能再快约 22%。
新成员二:Maglev(快速优化编译器,2023)
Sparkplug 快是快,但因为完全不优化,天花板很低——它和 TurboFan 之间还是隔着一道大鸿沟。Maglev 就是来填这道鸿沟的。
Maglev 位于 Sparkplug 和 TurboFan 之间,扮演"快速的优化编译器":它生成的代码比 Sparkplug 好得多(因为它真的做优化),但生成速度又远快于 TurboFan(因为它的优化比 TurboFan 轻量)。
这里有个值得记住的技术细节,也是面试深水区的好素材——Maglev 和 TurboFan 用了不同的内部表示(IR):
- TurboFan用的是"节点之海"(sea-of-nodes)——非常灵活、能做激进优化,但结构复杂、对 CPU 缓存不友好、编译慢。
- Maglev用的是更传统的SSA(静态单赋值)+ 控制流图(CFG)——结构更简单、编译更快,优化能力介于 Sparkplug 和 TurboFan 之间。
Maglev 同样会做投机优化、也能 deopt(它复用了 TurboFan 那套去优化机制,所以旧文讲的 deopt 概念在这里完全适用),只是整体更轻、更快。
为什么要这么设计:分层优化的哲学
把四层串起来看,V8 的思路是按代码的"热度"精准投放优化资源:
- 代码刚开始跑 → Ignition 解释执行,启动最快,不浪费编译成本;
- 跑了一些次数(温热)→ Sparkplug 给一层免费的机器码;
- 更热了 → Maglev 花点小成本做中等优化;
- 滚烫的热点 → TurboFan 砸重成本做极致优化。
这解决的正是旧架构的断层:每一档热度都有性价比合适的优化档位接着,不再是"要么太慢、要么太贵"的二选一。这套"tiering up"思想,是理解现代 V8 性能表现的钥匙。
二、垃圾回收:从"增量"到"并发/并行"
旧文的 GC 部分讲了引用计数、标记清除、分代、全停顿、增量标记、延迟清理、增量整理。这几年 GC 又有两个重要进展值得补上。
补充一:并行与并发(Orinoco 项目)
旧文提到的"增量标记"是减少全停顿的第一招——把标记拆成小片穿插进主线程空隙。但现代 V8 的 GC(统称Orinoco项目)把三种手段结合了起来,这三个词经常被混用,务必分清:
- 增量(incremental):把 GC 工作拆成小块,穿插在主线程的空闲间隙做。主线程还是要停,但每次停得短。
- 并行(parallel):主线程和多个辅助线程同时干 GC 的活。主线程仍需暂停,但因为多线程一起干,暂停时间大幅缩短。
- 并发(concurrent):辅助线程在主线程照常运行 JS 的同时,在后台默默做标记。主线程几乎不用停。
现代 V8 的组合大致是:新生代用并行 Scavenge,老生代用并发标记 + 并行清理/整理。这是旧文那个"全停顿"问题在这几年的进一步解法——从"拆小片穿插"进化到"多线程后台并发处理",主线程卡顿被压到了更低。
补充二:Oilpan —— 连 C++ 对象也有了 GC
旧文讲的是 JS 堆的垃圾回收。但这几年 V8 单独发展出了一套Oilpan(代码里叫 cppgc)——给 C++ 对象用的垃圾回收器。
这是干嘛的?浏览器里很多东西(比如 DOM 节点)底层是用 C++ 实现的对象,它们的内存也需要管理。Oilpan 就是给这些 C++ 对象做自动回收的。对前端来说,这是个能加分的认知:你操作的 DOM 节点,它的生命周期其实也被一套(C++ 的)GC 管理着——浏览器的内存管理不止 JS 堆这一层。
三、内存与安全:两个新方向
补充一:指针压缩(Pointer Compression,2020)
问题:64 位系统上,每个指针占 8 字节,对象里到处是指针,很占内存。V8 的观察是——堆通常不会超过 4GB,那么用 32 位就足够寻址了。于是 V8只存 32 位的偏移量(相对于一个基地址),把指针从 8 字节压到 4 字节。
效果:V8 堆内存显著下降(约 40%),对内存受限的移动设备尤其重要。
顺带解释一个概念——Smi(Small Integer,小整数):V8 把小整数用带标记的 31 位直接内联存进"指针位置",不用在堆上单独分配对象,既省内存又快。指针压缩正是和 Smi 这套编码配合工作的。
补充二:V8 Sandbox(V8 沙箱,2024)
这是 V8 安全方向的大事,也回应了一个现实矛盾:V8 为了极致性能用了大量底层优化(JIT、内联缓存、指针压缩等),而这些优化面恰恰是内存漏洞的高发区。任何一个边界检查疏忽、类型转换错误,都可能被利用成内存破坏漏洞。
V8 Sandbox 是一道新防线:它把 V8 堆的内存访问限制在一个受控的沙箱区域内。即便攻击者利用漏洞篡改了堆内对象,也很难逃逸到整个进程的地址空间去执行任意代码。配合另一项Control-flow Integrity(控制流完整性),V8 在"性能 vs 安全"这场持久博弈里补强了安全侧。
四、几个小而重要的变化
发布博客停了。:V8 从 2022 年 6 月起,不再为每个版本单独写发布博客了。现在想跟进新特性,要看 v8.dev 的 feature 页和 Chrome 的发布说明。
JIT-less 模式。V8 支持完全不生成机器码、纯 Ignition 解释执行的模式。用在 iOS 这类出于安全禁止运行时生成可执行代码的平台上。这解释了一个现象——同样是 V8,在 iOS 上的表现可能和别处不同,因为它跑在 JIT-less 模式下,吃不到那几层编译器的加速。
一批具体的性能专题(可作延伸阅读):更快的 async/await 和 Promise、super属性访问加速、JSON.stringify提速一倍多、堆快照(heap snapshot)速度大幅提升——最后这条正好和旧文的 GC / DevTools 内存分析呼应,抓内存泄漏更快了。
资源:所有更新都可以在 V8 官方博客 https://v8.dev/blog 找到一手出处(Sparkplug、Maglev、指针压缩、Oilpan、Sandbox 都有专文)。注意逐版本发布博客已于 2022 年停更,新特性改看 v8.dev 的 features 页。