- 语言运行时
- 编译器
- JIT编译
- 解释器
- 内存管理
【免费下载链接】v8
The official mirror of the V8 Git repository
本文以 V8 官方文档 docs/runtime/tiering.md 为核心骨架,结合仓库源码深入剖析 V8 的多层执行架构(Ignition → Sparkplug → Maglev → TurboFan/Turboshaft)、驱动函数升级(tier up)的Interrupt Budget(中断预算)机制,以及 OSR(栈上替换)与 JS Dispatch Table 的实现细节。读完本文,你将掌握 V8 如何在不牺牲启动速度的前提下逼近峰值性能,并能在源码中准确找到预算扣减、中断处理与优化决策的完整调用链。
执行分层(Execution Tiers)
V8 采用多层执行策略(multi-tiered execution),在启动速度与峰值性能之间取得平衡。当前仓库中实际生效的是四层体系:
| 层级 | 组件 | 定位 | 特点 |
|---|---|---|---|
| 1 | Ignition(解释器) | 执行字节码 | 启动快、内存开销低,但执行较慢;负责收集类型反馈(type feedback) |
| 2 | Sparkplug(基线编译器) | 字节码直译到机器码 | 编译极快、不做优化,性能优于 Ignition;见 src/baseline 与 src/baseline/baseline-compiler.cc |
| 3 | Maglev(中层优化编译器) | 基于图的优化编译器 | 编译快,利用类型反馈生成高质量代码;见 src/maglev/maglev-compiler.cc |
| 4 | TurboFan / Turboshaft(顶层优化编译器) | 高级优化编译器 | 编译慢,但生成最极致的优化代码,追求峰值性能;见 src/compiler 与 src/compiler/turboshaft |
函数默认从 Ignition 开始执行,随热度提升逐层"升温",最终到达顶层优化代码。这种渐进式策略让冷启动函数只付出解释执行的成本,而热点函数能享受到完整的优化管线。
Interrupt Budget:升温的触发器
Interrupt Budget(中断预算)是 V8 决定何时将函数提升(tier up)到更高层级的关键机制:
- 每个函数都关联一个预算,存储在它的
FeedbackCell中,具体字段为interrupt_budget_(src/objects/feedback-cell.h 中声明为int32_t interrupt_budget_;); - 预算根据当前执行层级和相关 flags 初始化为特定值(由
JSFunction::SetInterruptBudget设置,见 src/objects/js-function.h); - 函数执行过程中预算被逐步递减;
- 当预算降到零或负数时,V8 触发一次检查,判断该函数是否应当升级到更高层级。
预算的初始值不是固定的:SetInterruptBudget会根据函数是否已有 feedback vector以及是否已有优化代码来调整额度(见 src/objects/js-function.h 的注释 "Sets the interrupt budget based on whether the function has a feedback vector and any optimized code")。这意味着越"有价值"的函数,其升级节奏越快。
预算如何被递减
所有执行层级(Ignition、Sparkplug、Maglev)都会在策略性位置递减中断预算:
- 循环回边(Loop Back-Edges):用于检测热循环(hot loop);
- 函数返回(Function Returns):用于检测被频繁调用的函数。
Ignition(解释器)
在 src/interpreter/interpreter-assembler.cc 中,预算扣减由DecreaseInterruptBudget实现,它被两个关键位置调用:
JumpBackward(循环回边,见 src/interpreter/interpreter-assembler.cc);UpdateInterruptBudgetOnReturn(函数返回,见 src/interpreter/interpreter-assembler.cc)。
实际扣减逻辑在UpdateInterruptBudget中完成(src/interpreter/interpreter-assembler.cc):从JSFunction加载feedback_cell_,再读取其中的interrupt_budget_,用Int32Sub减去权重后写回;随后DecreaseInterruptBudget(src/interpreter/interpreter-assembler.cc)检查新预算是否小于零,若小于零则调用对应的 runtime 中断函数:
TNode<Int32T> new_budget = UpdateInterruptBudget(weight_after_bytecode); Branch(Int32GreaterThanOrEqual(new_budget, Int32Constant(0)), &done, &interrupt_check); BIND(&interrupt_check); TNode<JSFunction> function = LoadFunctionClosure(); CallRuntime(stack_check_behavior == kEnableStackCheck ? Runtime::kBytecodeBudgetInterruptWithStackCheck_Ignition : Runtime::kBytecodeBudgetInterrupt_Ignition, GetContext(), function);注意两个实现细节:
- 权重包含当前字节码大小:
weight_after_bytecode = weight + CurrentBytecodeSize(),即预算扣减把正在执行的字节码本身也算进去,避免无限循环中预算永不耗尽; - 循环回边使用带栈检查的变体:
JumpBackward调用DecreaseInterruptBudget(..., kEnableStackCheck),因此循环回边同时检查中断与栈溢出,而返回路径使用kDisableStackCheck(见 src/interpreter/interpreter-assembler.cc 附近)。
Sparkplug(基线)
在架构相关的汇编文件中(如 src/baseline/x64/baseline-assembler-x64-inl.h),Sparkplug 通过AddToInterruptBudgetAndJumpIfNotExceeded在返回点和循环回边处使用负权重扣减预算——即以"加上一个负值"的方式等价实现递减。
Maglev(中层)
在 Maglev IR(中间表示)中,预算扣减由ReduceInterruptBudgetForLoop与ReduceInterruptBudgetForReturn两个节点表示。
示例:Maglev x64 上的实现
在 src/maglev/x64/maglev-ir-x64.cc 中,预算扣减通过从FeedbackCell中减去指定金额实现:
void GenerateReduceInterruptBudget(MaglevAssembler* masm, Node* node, Register feedback_cell, ReduceInterruptBudgetType type, int amount) { MaglevAssembler::TemporaryRegisterScope temps(masm); __ subl( FieldOperand(feedback_cell, offsetof(FeedbackCell, interrupt_budget_)), Immediate(amount)); ZoneLabelRef done(masm); __ JumpToDeferredIf(less, HandleInterruptsAndTiering, done, node, type); __ bind(*done); }若减法结果为负数(less条件),则跳转到延迟代码(deferred code)HandleInterruptsAndTiering处理中断与升级。这里体现了 Maglev 生成代码的惯用模式:把不常走的分支放到 deferred 区域,保证热路径上只有一条subl加一次条件跳转,开销极小。
处理中断:进入 C++ Runtime
当预算耗尽时,V8 调用 C++ runtime。每个层级都有自己的一组 runtime 函数:
- Ignition:
Runtime::kBytecodeBudgetInterruptWithStackCheck_Ignition与Runtime::kBytecodeBudgetInterrupt_Ignition; - Sparkplug:
Runtime::kBytecodeBudgetInterruptWithStackCheck_Sparkplug与Runtime::kBytecodeBudgetInterrupt_Sparkplug; - Maglev:
Runtime::kBytecodeBudgetLoopInterrupt_Maglev与Runtime::kBytecodeBudgetInterrupt_Maglev。
"WithStackCheck" 变体通常用于循环回边,以便在同一时刻一并处理两类检查:
- 中断(interrupts):如 GC 请求、调试器暂停、
--stress-*测试注入等; - 栈限制检查(stack limit):检测栈溢出风险。
这些 runtime 函数最终都会经由TieringManager::OnInterruptTick汇聚到**TieringManager**(层级管理器)中。
升级决策:TieringManager
TieringManager的核心入口是OnInterruptTick(DirectHandle<JSFunction> function, CodeKind code_kind)(声明见 src/execution/tiering-manager.h),其实现位于 src/execution/tiering-manager.cc。它根据函数的性能剖析数据(feedback vector、调用次数 invocation count 等)做出决策:
- 判断函数是否足够"热":是否值得优化;
- 决定目标层级:例如 Maglev 已启用且函数当前在 Sparkplug,则可能以 Maglev 为目标;
- 将函数排队进行后台并发编译(concurrent compilation)。
从源码看,OnInterruptTick内部还有几层精细逻辑:
- 首次升级到 Sparkplug 的判定:
FirstTimeTierUpToSparkplug负责记录函数第一次从 Ignition 升到 Sparkplug 的时刻;若此前没有 feedback vector,会先调用JSFunction::CreateAndAttachFeedbackVector创建并挂载反馈向量(并把 invocation count 初始化为 1,为 OSR 内联做准备); - 避免 OSR 中途触发 GC:
maybe_had_optimized_osr_code检查后,若有 OSR 优化代码在途,则跳过 Sparkplug 编译; - 批量编译开关:
v8_flags.baseline_batch_compilation为真时,函数交给BaselineBatchCompiler排队批量编译,否则同步调用Compiler::CompileBaseline; - 禁用优化时的兜底:若
!isolate_->use_optimizer()(TurboFan 被禁用),则不升级,只调用SetInterruptBudget(BudgetModification::kRaise)重置预算; - 防止重复升级:
MaybeOptimizeFrame(src/execution/tiering-manager.cc)首先检查tiering_in_progress与osr_tiering_in_progress,函数处于编译中时直接跳过本次升级(TraceInOptimizationQueue),避免并发触发同一次编译;在concurrent_recompilation_front_running开启时,可提高既有 Maglev 编译任务中 TurboFan 的优先级。
后台编译完成后,V8 会更新函数在JS Dispatch Table中的条目,使后续调用自动切换到新代码。
JS Dispatch Table 与句柄(Handles)
在现代 V8 中(尤其是启用V8 Sandbox时),一个JSFunction并不直接指向它的Code对象,而是持有一个JSDispatchHandle——它是JSDispatchTable中的索引:
- 间接层(Indirection):
JSDispatchTable保存实际的Code指针(以及入口点 entry point); - 更新(Update):升级时 V8不修改
JSFunction对象本身,而是更新该函数句柄所指的JSDispatchTable条目; - 原子性(Atomicity):这使代码入口点的更新具备原子性,保证了线程安全,让并发执行与并发升级无需在函数对象上加复杂锁。
由于后续调用都经由同一个 dispatch handle 间接跳转,函数未来被调用时自动使用新代码,无需修改任何调用点。
栈上替换(OSR)
OSR(On-Stack Replacement,栈上替换)允许 V8 在函数**正在执行(位于调用栈上)**时,把执行从非优化代码(Ignition)切换到优化代码(Maglev/TurboFan)。
为什么需要 OSR
如果一个函数包含一个长时间运行的循环、但自身调用频率不高,仅靠返回点的预算中断不会及时触发优化——函数迟迟不返回,优化永远轮不到它。循环回边上的预算扣减正好解决这个问题:在循环内部就触发优化。
工作流程
- 检测(Detection):当某个循环回边的预算归零时,V8 检查 OSR 是否适用;
- 编译(Compilation):V8 用 Maglev 或 TurboFan 编译该函数,但以特定循环头(loop header)作为入口点,而非正常的函数起始位置;
- 替换(Replacement):解释器栈帧被替换为优化栈帧(帧重建,frame reconstruction),执行在优化代码的循环头处继续。
OSR 在TieringManager中的痕迹清晰可见:MaybeOptimizeFrame会读取function->feedback_vector()->osr_tiering_in_progress()与maybe_has_optimized_osr_code()来协调 OSR 编译与其他升级动作(src/execution/tiering-manager.cc),并刻意避免在 OSR 过程中触发 GC。
扩展阅读
- src/execution/tiering-manager.cc:层级管理器核心实现,含
OnInterruptTick与MaybeOptimizeFrame; - src/maglev/maglev-compiler.cc:Maglev 编译管线;
- src/baseline/baseline-compiler.cc:Sparkplug 基线编译器;
- src/interpreter/interpreter-assembler.cc:Ignition 解释器的预算扣减实现;
- src/objects/feedback-cell.h:
interrupt_budget_字段定义; - src/objects/js-function.h:
SetInterruptBudget与 dispatch handle 相关接口; - docs/compiler/concurrency-and-background-compilation.md:后台并发编译与分层执行的配合;
- docs/runtime/function-architecture.md:函数对象整体架构。
- 语言运行时
- 编译器
- JIT编译
- 解释器
- 内存管理
【免费下载链接】v8
The official mirror of the V8 Git repository
相关推荐
niri 从源码到桌面会话:一份带踩坑记录的编译部署笔记
niri 从源码到桌面会话:一份带踩坑记录的编译部署笔记 niri 是一个可滚动平铺的 Wayland 合成器:窗口自动排成列,用滚轮在多个空间间穿梭,适合想要
语言运行时编译器JIT编译解释器内存管理V8 Sparkplug 基线编译器深度解析:从 Ignition 字节码到机器码的即时编译通道
V8 Sparkplug 基线编译器深度解析:从 Ignition 字节码到机器码的即时编译通道 Sparkplug 是 V8 引擎的基线编译器(Baselin
语言运行时编译器JIT编译解释器内存管理V8 Hidden Classes(Maps)深度教程:从对象结构追踪到 TurboFan 常量折叠
V8 Hidden Classes(Maps)深度教程:从对象结构追踪到 TurboFan 常量折叠 本文以 V8 官方隐藏类教程为主线,系统讲解 V8 如何通
语言运行时编译器JIT编译解释器内存管理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考