V8 引擎运行 JS代码 — 解释&编译
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代码编译转换成字节码和部分机器码。步骤如下:
💡
1. 解析器将 JS Code 解析转换成 AST 并分析出关联的 Scopes 作用域, 构建AST 的过程称之为解析 Parse。
2. 随后进行第一次编译 AST + Scopes 编译转换成字节码, 第一个编译器名字叫做 Ignition ,ignition 将 AST 转变成 bytecode 字节码。
3. 解释执行字节码。字节码被解释器 inceptor 解释执行,解释执行过程中会进行代码优化标记,。
4. IC 优化。
5. 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 提供的字节码映射表:
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
// 原代码
function add(a, b) {
return a + b;
}
function calc(x, y) {
return add(x, y) * 2;
}
// 内联优化后
function calc(x, y) {
return (x + y) * 2;
}
内联优化标记策略:
💡
1. 构造函数调用或者普通函数调用
2. 存在直接递归调用的不优化
3. 未达到最低调用频率不优化
4. 强制内联小函数【27字节码以内,转换JS代码大概1-2行】注意:即使是小函数不符合前三条也不会优化!
5. 考虑内存和性能预算,当内存预算到了瓶颈停止内联,内存占用超过预算甚至会将部分内联进行退化。小函数优势在此!
// file: src/compiler/js-inlining-heuristic.cc
Reduction 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();
}
// Returns true if opcode can be inlined.
static bool IsInlineeOpcode(Value value) {
return value == kJSConstruct || value == kJSCall;
}
💡
opcode 常见值如下:
1. **kJSCall**:表示一个普通的 JavaScript 函数调用。
2. **kJSConstruct**:表示一个构造函数调用,通常是通过 **`new`** 关键字调用的函数。
3. **kJSReturn**:表示返回语句,结束当前函数并返回值。
4. **kJSLoadProperty**:表示加载对象的属性。
5. **kJSStoreProperty**:表示将值存储到对象的属性中。
6. **kJSLoadElement**:表示加载数组或类数组对象的元素。
7. **kJSStoreElement**:表示将值存储到数组或类数组对象的元素中。
8. **kJSThrow**:表示抛出异常。
9. **kJSBranch**:表示条件分支或跳转。
标记完成后,需要进行优化前检查,排序。到这里的基本都是非小函数【大概是函数体超过2行,小函数字节码限制27字节】。
// 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<int>(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。
- 稳定性检查,出现过不稳定执行的函数中止优化!
- 函数内逐步进行内联优化!直到总内联优化占用达到上限!
Reduction 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字节
// file : src/flags/flag-definitions.h
DEFINE_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 会观察代码的执行模式,通过内联缓存和隐藏类等信息,对代码进行进一步优化,使其达到最佳性能。
// file: src/execution/tiering-manager.cc
void TieringManager::OnInterruptTick(DirectHandle<JSFunction> 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<JSFunction> 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<FeedbackVector> feedback_vector, CodeKind current_code_kind) {
Tagged<SharedFunctionInfo> 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<BytecodeArray> 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) https://v8.dev/blog/maglev
- [how-v8-javascript-engine-works](https://cabulous.medium.com/how-v8-javascript-engine-works-5393832d80a7) 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) [https://www.youtube.com/watch?v=p-iiEDtpy6I](https://www.youtube.com/watch?v=p-iiEDtpy6I)
- V8 [github.com/v8](https://github.com/v8/v8)