v8引擎系列之解释&编译

v8引擎系列之解释&编译

V8 引擎是 JS 运行引擎中的一种,在浏览器和node中比较常见。新开V8引擎系列文章,研究探索引擎背后的调用流程和设计思路。V8引擎系列文章主要参考V8引擎开源源码,辅助开源社区资料(部分资料已经过时)进行总结。如有总结错误之处,望不吝赐教,在此拜谢。

前置-背景什么是 V8引擎V8 是一个C++编写的程序,它用于编译执行JavaScript代码。提供以下基础能力:

💡1、编译并执行JS代码

2、以某种顺序执行函数,处理调用栈

3、管理对象的内存分配,堆内存

4、垃圾回收

5、提供所有 JS 语言支持的数据类型、运算符、API 和公共函数

V8 提供可选事件循环(浏览器中运行JS时,事件循环由浏览器提供)。V8 是一个单线程运行JS代码的多线程应用,V8引擎本身是多线程程序,V8采用单线程运行JS代码。

💡V8运行JS代码是是单线程。一个V8实例,运行一个单独的JS执行上下文。在浏览器或者 node.js 开发的进程中可以同时存在多个V8线程实例来实现多线程并发,比如:通过WebWorker等技术开辟新的执行JS上下文并调用V8线程执行。

V8 引擎内部多线程大体划分为几类:

💡1、主线程:负责执行 JS 代码解析、编译、运行。处理所有关于 JS 代码执行相关的任务,包括调用栈管理和事件循环。

2、垃圾回收线程:负责自动管理内存。

3、TurboFan 编译线程: V8 执行JS代码过程中会标记 hot 代码,TurboFan 线程会将 hot 代码编译优化,通常hot代码会被转换为机器码。

4、I/O线程:node.js 环境中与 libuv 集成。负责文件系统操作,网络请求等等I/O操作。

5、其他辅助线程:debug、性能分析等等

V8 引擎运行 JS 代码流程大体如下:

V8 只负责执行 JS,部分运行环境需要调用宿主提供。

Host Env 运行环境V8的运行宿主环境有多种,最为常见的是浏览器 Browser 和 NodeJS 。Browser 环境和 NodeJS 环境有相同的成员如:ECMAScript standard (JavaScript核心内置API)、函数调用栈、Heap 内存、垃圾回收等等。不一致的在于特殊 API + 事件循环。

特殊点如下:

Browser 环境:

💡Web API: 浏览器提供的接口,用于程序和浏览器交互 Canvas、ServiceWorker、WebStorage、Fetch、 Audio/Video、Geolocation等等

DOM API:文档对象模型,用于对 HTML 进行操作。主要包含以下方面 API 功能Element CRUD、Event Handing 事件监听处理、CSSOM 样式表修改

Event Loop: 浏览器单独提供的时间循环与渲染周期结合

NodeJS 环境:

💡文件系统 API: fs 模块,解决 i/o 读写

网络 API:http、https、net等模块

环境变量 proces :环境变量维护

Cluster集群和子线程:多线程并发API

其他模块: C++ 模块

Event Loop:NodeJS 的 Event Loop采用了多阶段设计,允许更细粒度的控制

事件驱动模型中唤起的回调,经过事件循环调用V8引擎执行。页面事件/浏览器生命周期/WebAPI 等等来源触发的回调函数,被添加到事件循环的任务队列中【这里也可称之为宏任务队列】。事件循环与微任务队列的关系在本文中不会深入,会单开一文。

以浏览器环境调用V8运行JS为例

到此,正式进入 JS 代码如何在引擎中运行过程。第一步是:解释执行编译优化

编译 & 运行过程V8 引擎将JS代码编译转换成字节码和部分机器码。步骤如下:

💡

解析器将 JS Code 解析转换成 AST 并分析出关联的 Scopes 作用域, 构建AST 的过程称之为解析 Parse。

随后进行第一次编译 AST + Scopes 编译转换成字节码, 第一个编译器名字叫做 Ignition ,ignition 将 AST 转变成 bytecode 字节码。

解释执行字节码。字节码被解释器 inceptor 解释执行,解释执行过程中会进行代码优化标记,。

IC 优化。

Hot 代码三级优化,转成机器码。针对Hot代码,V8引擎目前采用三级优化模式。分别是:Sparkplug、Maglev、TurboFan 。三位大师负责将标记为 Hot 代码进一步编译成为机器码,以此提升代码运行速度。机器码被 CPU 直接运行。

到了这里会有两个疑问,为什么不全部编译成为机器码?第二什么情况下会进行优化,什么情况下会退出优化?

💡

最快的启动运行和不太大的内存。

JIT 编译成为字节码能够节约时间,直接编译成机器码需要分析整个JS脚本推断所有对象类型和优化条件。Web 站点快速加载、响应流畅需要最快启动JS脚本执行。

大部分JS代码不会重复运行,运行初始编译成机器码耗时多且内存占用非常大。

JS 语言特性:动态类型不确定性

运行时频繁修改变量类型,未运行前无法推断准确的类型。

通过解释器 Ignition 运行字节码时,标记变量类型结构固定且频繁执行的 代码为 Hot,将Hot代编译成码机器码提升运行速率。

跨平台兼容性

bytecode 与平台无关,可以在不同的硬件架构上运行。机器码由 CPU 直接运行,与 CPU 架构以及支持的编码强相关。

字节码 VS 机器码编译有两种常见的模式 JIT (just in time)即时编译和 AOT(Ahead of time)提前编译。JIT 是在代码运行期间动态编译,编译-运行-编译-运行。AOT 一般常见于C++ 和 JAVA 这类强类型语言,打包构建时直接产出字节码或机器码。V8 引擎编译JS代码采用的是 JIT编译模式。

V8 执行 JS 代码过程中先翻译成 bytecode, 多次运行会标记可被优化的代码标记为 Hot TuboFan 将 Hot 代码从 bytecode 编译成机器码,加速运行【CPU直接运行机器码】。

V8 提供的字节码映射表:

https://github.com/v8/v8/blob/master/src/interpreter/bytecodes.h

10k 的 JS 代码就全编译成机器码需要 20M 的空间,1M的JS代码则需要约 2G 的内存空间。

V8采用折中方案:解释执行大部分代码,对部分高频代码进行优化。

V8 引擎运行优化V8 引擎优化 “在内存、CPU、等资源合适的情况下,优化调用频繁;类型结构稳定;执行时间长;代码规模适中;代码控制流程简单的代码” 提升运行效率。剔除死代码,死代码包括:不可达代码;无用计算;冗余的类型检查。

在分析优化逻辑之前,需要先介绍一下 feedback_vector 。JS 代码在V8引擎中运行过程中,所有的代码分析 / 运行记录 等信息都是记录在 feedback_vector 上。

在 JS source → bytecode -> intercept execute - … → Maglev … → TurboFan → MachineCode .. 过程中代码的分析结果,运行中的返回值、稳定性、耗时等信息都存储在 feedback_vector 上.

💡分类介绍 feedback_vector 存储的重要信息

1、代码编译分析信息:

2、运行时优化信息

V8 基于 各个环节提取的 feedback_vector 信息进行优化/去优化分析。包括:死代码消除、IC 内联代码优化、Hot 标记优化。

标记优化的大流程如下:

死代码消除V8 对代码进行标记,不可达的代码;无用计算的代码;冗余的类型检查等等代码会被标记为 isDead。这些代码不会被执行也不会被内联优化检查到!

内联优化 【src/compiler/js-inlining-heuristic.cc】减少函数调用开销(调用栈切换);减少局部变量访问次数来提升实现优化。函数体积小、调用频率高、参数类型稳定、没有复杂控制流。一句话,高频调用且可预测的小函数。

内联优化的小 case

123456789101112// 原代码function add(a, b) { return a + b;}function calc(x, y) { return add(x, y) * 2; }// 内联优化后function calc(x, y) { return (x + y) * 2;}

内联优化标记策略:

💡

构造函数调用或者普通函数调用

存在直接递归调用的不优化

未达到最低调用频率不优化

强制内联小函数【27字节码以内,转换JS代码大概1-2行】注意:即使是小函数不符合前三条也不会优化!

考虑内存和性能预算,当内存预算到了瓶颈停止内联,内存占用超过预算甚至会将部分内联进行退化。小函数优势在此!

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960// file: src/compiler/js-inlining-heuristic.ccReduction JSInliningHeuristic::Reduce(Node* node) { // 非构造函数调用或者普通函数调用 退出检查 if (!IrOpcode::IsInlineeOpcode(node->opcode())) return NoChange(); // 累计内联大小超过上限--退出 if (total_inlined_bytecode_size_ >= max_inlined_bytecode_size_absolute_) { return NoChange(); } ... for (int i = 0; i < candidate.num_functions; ++i) { // 直接递归调用---不优化 if (frame_info.shared_info().ToHandle(&frame_shared_info) && frame_shared_info.equals(shared.object())) { TRACE("Not considering call site #" << node->id() << ":" << node->op()->mnemonic() << ", because of recursive inlining"); candidate.can_inline_function[i] = false; } // 强制优化小函数 if (candidate.can_inline_function[i]) { can_inline_candidate = true; BytecodeArrayRef bytecode = candidate.bytecode[i].value(); candidate.total_size += bytecode.length(); unsigned inlined_bytecode_size = 0; if (OptionalJSFunctionRef function = candidate.functions[i]) { if (OptionalCodeRef code = function->code(broker())) { inlined_bytecode_size = code->GetInlinedBytecodeSize(); candidate.total_size += inlined_bytecode_size; } } candidate_is_small = candidate_is_small && IsSmall(bytecode.length() + inlined_bytecode_size); } } if (!can_inline_candidate) return NoChange(); // 未达到最低调用频率不优化:min_inlining_frequency = 0.15 if (candidate.frequency.IsKnown() && candidate.frequency.value() < v8_flags.min_inlining_frequency) { return NoChange(); } seen_.insert(node->id()); // 小函数强制内联--- if (candidate_is_small) { return InlineCandidate(candidate, true); } // In the general case we remember the candidate for later. candidates_.insert(candidate); return NoChange();}

1234// Returns true if opcode can be inlined.static bool IsInlineeOpcode(Value value) { return value == kJSConstruct || value == kJSCall;}

💡opcode 常见值如下:

kJSCall:表示一个普通的 JavaScript 函数调用。

kJSConstruct:表示一个构造函数调用,通常是通过 new 关键字调用的函数。

kJSReturn:表示返回语句,结束当前函数并返回值。

kJSLoadProperty:表示加载对象的属性。

kJSStoreProperty:表示将值存储到对象的属性中。

kJSLoadElement:表示加载数组或类数组对象的元素。

kJSStoreElement:表示将值存储到数组或类数组对象的元素中。

kJSThrow:表示抛出异常。

kJSBranch:表示条件分支或跳转。

标记完成后,需要进行优化前检查,排序。到这里的基本都是非小函数【大概是函数体超过2行,小函数字节码限制27字节】。

12345678910111213141516171819202122232425262728293031323334// file: src/compiler/js-inlining-heuristic.cc// 排序策略void JSInliningHeuristic::Finalize() {... while (!candidates_.empty()) { auto i = candidates_.begin(); Candidate candidate = *i; candidates_.erase(i); // 已经优化了的--跳过 if (!IrOpcode::IsInlineeOpcode(candidate.node->opcode())) continue; // 无效死代码,跳过 if (candidate.node->IsDead()) continue; // 预算检查--- double size_of_candidate = candidate.total_size * v8_flags.reserve_inline_budget_scale_factor; int total_size = total_inlined_bytecode_size_ + static_cast(size_of_candidate); // 超过内联预算上限---本次暂时不优化 if (total_size > max_inlined_bytecode_size_cumulative_) { info_->set_could_not_inline_all_candidates(); // Try if any smaller functions are available to inline. continue; } // 恭喜!!进入下一环节~ Reduction const reduction = InlineCandidate(candidate, false); if (reduction.Changed()) return; }}

分析函数历史执行情况!从参数格式结构稳定性、运行结果稳定性、异常错误等方面进行评估,内容来自 feedback_vector。

稳定性检查,出现过不稳定执行的函数中止优化!

函数内逐步进行内联优化!直到总内联优化占用达到上限!

123456789101112131415161718192021222324252627282930313233343536373839Reduction JSInliningHeuristic::InlineCandidate(Candidate const& candidate, bool small_function) { ... // 检查调用是否稳定--参数检查! Node* if_exception = nullptr; if (NodeProperties::IsExceptionalCall(node, &if_exception)) { Node* if_exceptions[kMaxCallPolymorphism + 1]; // 检查历史执行情况:运行稳定性、异常结果检查 for (int i = 0; i < num_calls; ++i) { if_successes[i] = graph()->NewNode(common()->IfSuccess(), calls[i]); if_exceptions[i] = graph()->NewNode(common()->IfException(), calls[i], calls[i]); } // Morph the {if_exception} projection into a join. ... }... // 稳定性检查通过后,逐步进行内联优化!直到总内联优化占用达到上限! for (int i = 0; i < num_calls && total_inlined_bytecode_size_ < max_inlined_bytecode_size_absolute_; ++i) { if (candidate.can_inline_function[i] && (small_function || total_inlined_bytecode_size_ < max_inlined_bytecode_size_cumulative_)) { Node* call = calls[i]; Reduction const reduction = inliner_.ReduceJSCall(call); if (reduction.Changed()) { total_inlined_bytecode_size_ += candidate.bytecode[i]->length(); call->Kill(); } } } return Replace(value);}

内联优化存在几个针对字节码大小的限制,

💡

小函数内联大小:27字节

单个内联最大字节码:460字节

累计内联上限:920字节

123456789101112131415161718// file : src/flags/flag-definitions.hDEFINE_INT(max_inlined_bytecode_size, 460, "maximum size of bytecode for a single inlining")DEFINE_INT(max_inlined_bytecode_size_cumulative, 920, "maximum cumulative size of bytecode considered for inlining")DEFINE_INT(max_inlined_bytecode_size_absolute, 4600, "maximum absolute size of bytecode considered for inlining")DEFINE_FLOAT( reserve_inline_budget_scale_factor, 1.2, "scale factor of bytecode size used to calculate the inlining budget")DEFINE_INT(max_inlined_bytecode_size_small, 27, "maximum size of bytecode considered for small function inlining")DEFINE_INT(max_optimized_bytecode_size, 60 * KB, "maximum bytecode size to " "be considered for turbofan optimization; too high values may cause " "the compiler to hit (release) assertions")DEFINE_FLOAT(min_inlining_frequency, 0.15, "minimum frequency for inlining")DEFINE_BOOL(polymorphic_inlining, true, "polymorphic inlining")

其他不会被优化的场景:

💡

动态的 eval

with 语句

复杂的 try catch

单个函数编译为字节码内存超过 460 字节

调用评率低:ignition 为 0.15 [单位没看懂]

动态 import

非纯函数,存在不可预测的过程

内联优化是通过对解释执行的字节码进行执行过程中分析转换的,内联优化后产物还是字节码,Hot 代码中内联优化的代码则会一同编译成机器码。Hot 代码编译机器码部分,不再追溯关于内联优化相关内容。

Hot 代码优化V8 代码在字节码解释执行阶段会附带标记Hot代码(附带信息存于feedback_vector),通过将 Hot 代码从字节码编译成机器码。机器码由CPU直接运行效率远远大于ignition解释器运行字节码。

V8 引擎采用多级优化策略(平衡编译耗时和机器码优化深度),截止24年11月,V8发布源码中采用的是三级编译器。 Maglev 编译器第一版本发布是在 chrome 117 。

先介绍一下Hot代码涉及的优化编译器:

三级优化模型,V8目的是平衡编译成本和运行速度。编译成本:编译时间 + 内存占用。编译速度Sparkplug最快。以下是编译的速度:

V8 引擎解释器+编译器优化历史组合跑分情况:

💡

1. Ignition(解释器)

阶段:Ignition 是 V8 的字节码解释器,它是 JavaScript 执行的第一个阶段。

功能:将 JavaScript 源代码编译为字节码,并逐条解释执行。这种解释执行通常适合初始化阶段或短期、低频执行的代码。

调用条件:代码首次加载时,Ignition 会将源代码转为字节码,然后解释执行。

优点:解释器生成字节码的速度较快、内存开销小,但由于每次都需要解释,性能会比编译的机器码稍慢。

2. Sparkplug(基线编译器)

阶段:在 Ignition 之后,作为快速编译阶段执行。

功能:Sparkplug 是 V8 的基线编译器,负责将字节码快速编译为机器码,来提高代码执行速度。相比解释执行,编译后的代码运行更快,但 Sparkplug 并不会进行复杂优化,因此编译时间也非常短。

调用条件:当代码被标记为“热”代码; 不存在 feedback_vector;

优点:快速生成机器码,提升代码性能的同时仍保持较低内存消耗。

3. Maglev(中层编译器)

阶段:位于 Sparkplug 和 TurboFan 之间。

功能:Maglev 是一种中层即时编译器,用于在代码“热度”增加但尚未进入深度优化阶段时提供进一步的优化。Maglev 生成的机器码质量高于 Sparkplug,但没有 TurboFan 的复杂优化。

调用条件:Sparkplug 优化代码执行优化后,代码类型允许升级到 Maglev;之前没有Maglev 编译失败记录;未启动PGO(Profile Guided Optimization),启动PGO的直奔TurboFan;

优点:提供更高质量的机器码,在提高性能的同时避免过多调用 TurboFan。

4. TurboFan(优化编译器)

阶段:在代码被频繁调用后,为性能的最后提升执行。

功能:TurboFan 是 V8 的优化编译器,对代码进行深度优化,生成高度优化的机器码。它适合执行频率非常高、长时间运行的“热”代码。

调用条件:代码执行频率极高时触发,特别是在 Maglev 生成的代码仍不够高效的情况下,才会进入 TurboFan 的深度优化阶段。

优点:TurboFan 会观察代码的执行模式,通过内联缓存和隐藏类等信息,对代码进行进一步优化,使其达到最佳性能。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144// file: src/execution/tiering-manager.ccvoid TieringManager::OnInterruptTick(DirectHandle function, CodeKind code_kind) { ... // 第一步:先检查是否可使用 Sparkplug 优化:【函数没有feedback_vector】 const bool compile_sparkplug = CanCompileWithBaseline(isolate_, function->shared()) && function->ActiveTierIsIgnition(isolate_) && !maybe_had_optimized_osr_code; if (compile_sparkplug) {#ifdef V8_ENABLE_SPARKPLUG // sparkpug 入口。。。 if (v8_flags.baseline_batch_compilation) { isolate_->baseline_batch_compiler()->EnqueueFunction(function); } // if (first_time_tiered_up_to_sparkplug) { if (had_feedback_vector) { if (function->shared()->cached_tiering_decision() == CachedTieringDecision::kPending) { function->shared()->set_cached_tiering_decision( CachedTieringDecision::kEarlySparkplug); } function->SetInterruptBudget(isolate_); } return; } // Sparkplug 优化后升级的场景:Maglev、turboFan MaybeOptimizeFrame(function_obj, code_kind); ...}//----------------------------------------------------------void TieringManager::MaybeOptimizeFrame(Tagged function, CodeKind current_code_kind) {... if (V8_UNLIKELY(v8_flags.always_osr)) { TryRequestOsrAtNextOpportunity(isolate_, function); // Continue below and do a normal optimized compile as well. } const bool maglev_osr = maglev::IsMaglevOsrEnabled(); const CodeKinds available_kinds = function->GetAvailableCodeKinds(isolate_); const bool waiting_for_tierup = (current_code_kind < CodeKind::TURBOFAN_JS && (available_kinds & CodeKindFlag::TURBOFAN_JS)) || (maglev_osr && current_code_kind < CodeKind::MAGLEV && (available_kinds & CodeKindFlag::MAGLEV)); if (function->IsOptimizationRequested(isolate_) || waiting_for_tierup) { // 节能模式或电池节省模式 --- 不优化 if (V8_UNLIKELY(maglev_osr && current_code_kind == CodeKind::MAGLEV && (!v8_flags.osr_from_maglev || isolate_->EfficiencyModeEnabledForTiering() || isolate_->BatterySaverModeEnabled()))) { return; } // OSR kicks in only once we've previously decided to tier up, but we are // still in a lower-tier frame (this implies a long-running loop). // 需要优化:但是优先级不够的,提供优化:大循环 TryIncrementOsrUrgency(isolate_, function); return; } OptimizationDecision d = ShouldOptimize(function->feedback_vector(), current_code_kind); // We might be stuck in a baseline frame that wants to tier up to Maglev, but // is in a loop, and can't OSR, because Maglev doesn't have OSR. Allow it to // skip over Maglev by re-checking ShouldOptimize as if we were in Maglev. if (V8_UNLIKELY(!isolate_->EfficiencyModeEnabledForTiering() && !maglev_osr && d.should_optimize() && d.code_kind == CodeKind::MAGLEV)) { bool is_marked_for_maglev_optimization = existing_request == CodeKind::MAGLEV || (available_kinds & CodeKindFlag::MAGLEV); // 优化第二级:Maglev 优化检查进入 if (is_marked_for_maglev_optimization) { d = ShouldOptimize(function->feedback_vector(), CodeKind::MAGLEV); } } // 进入终极优化: turboFan if (d.should_optimize()) Optimize(function, d);}//-------------------------------OptimizationDecision TieringManager::ShouldOptimize( Tagged feedback_vector, CodeKind current_code_kind) { Tagged shared = feedback_vector->shared_function_info(); // 已经优化过的代码,不处理 if (current_code_kind == CodeKind::TURBOFAN_JS) { return OptimizationDecision::DoNotOptimize(); } if (TiersUpToMaglev(current_code_kind) && shared->PassesFilter(v8_flags.maglev_filter) && !shared->maglev_compilation_failed()) { if (v8_flags.profile_guided_optimization && shared->cached_tiering_decision() == CachedTieringDecision::kEarlyTurbofan) { // 进入TurboFan 优化 return OptimizationDecision::TurbofanHotAndStable(); } // Maglev 优化 return OptimizationDecision::Maglev(); } if (V8_UNLIKELY(!v8_flags.turbofan || !shared->PassesFilter(v8_flags.turbo_filter) || (v8_flags.efficiency_mode_disable_turbofan && isolate_->EfficiencyModeEnabledForTiering()) || isolate_->BatterySaverModeEnabled())) { return OptimizationDecision::DoNotOptimize(); } // invocation_count 函数的调用次数!!efficiency_mode_delay_turbofan 初始值15000 if (isolate_->EfficiencyModeEnabledForTiering() && v8_flags.efficiency_mode_delay_turbofan && feedback_vector->invocation_count() < v8_flags.efficiency_mode_delay_turbofan) { return OptimizationDecision::DoNotOptimize(); } // 优化的字节码超过上限:不优化 max_optimized_bytecode_size 初始值60KB Tagged bytecode = shared->GetBytecodeArray(isolate_); if (bytecode->length() > v8_flags.max_optimized_bytecode_size) { return OptimizationDecision::DoNotOptimize(); } // 进入 TurboFan 优化 return OptimizationDecision::TurbofanHotAndStable();}

以上就是Hot代码分层优化的内容,什么时候去优化呢?当内存资源出现压力后、函数变冷(调用不再频繁)等情况优化会被去掉,退回字节码解释执行。

如何写好代码?思考题~

参考资料💡

Maglev - V8最快的JIT优化 https://v8.dev/blog/maglev

how-v8-javascript-engine-works https://cabulous.medium.com/how-v8-javascript-engine-works-5393832d80a7

Franziska Hinkelmann: JavaScript engines - how do they even? | JSConf EU https://www.youtube.com/watch?v=p-iiEDtpy6I

V8 github.com/v8

相关推荐

拳皇97怎么放技能?简单攻略助你轻松上手 拳皇97怎么放技能?简单攻略助你轻松上手
形容愁怨的词语(形容愁和恨的成语有哪些)(40个)
沙巴体育365体育网站

形容愁怨的词语(形容愁和恨的成语有哪些)(40个)

📅 09-07 🔥 169
西班牙历史世界杯比分纪录(西班牙国家队的历届世界杯比分统计及关键战绩)
谥号“僖”:褒贬交织中的历史镜像
365bet充值方式

谥号“僖”:褒贬交织中的历史镜像

📅 08-10 🔥 988
张嘉文直播录像
365bet充值方式

张嘉文直播录像

📅 08-31 🔥 591
《魔兽世界》怀旧服暴风城声望速冲攻略
沙巴体育365体育网站

《魔兽世界》怀旧服暴风城声望速冲攻略

📅 09-02 🔥 585