测试代码
// 触发 Smi
const smiVal = 42;
%DebugPrint(smiVal); // 使用 V8 内部函数打印对象信息
// 触发 HeapNumber
const heapNumberVal = 3.14;
const s = "V8"; // 预分配为 String 对象
const obj = { x: 1 }; // 预分配隐藏类(Hidden Class)
%DebugPrint(heapNumberVal);
// 强制转换为 HeapNumber
const converted = Number(smiVal); // 调用 Number 构造函数
%DebugPrint(converted);
const sum = smiVal + heapNumberVal;
执行结果
从执行结构来看,smiVal和converted属于smi对象,heapNumberVal属于heapNumber对象。
对比smi和heap object
在 V8 引擎中,Smi(Small Integer) 和 堆对象(Heap Object) 是两种不同的数据存储机制,它们在内存管理、性能优化和使用场景上有显著区别。以下是它们的核心差异:
1. 存储方式
特性 | Smi | 堆对象(如 HeapNumber、Object) |
---|---|---|
存储位置 | 直接存储在 指针本身(无需堆内存分配) | 存储在 堆内存 中(需显式分配) |
编码规则 | 小整数左移一位,最低位(LSB)标记为 0 | 堆地址右移一位,最低位标记为 1 |
内存占用 | 无额外内存占用(仅指针空间) | 需要堆内存(通常 8-16 字节,取决于类型) |
示例
• Smi:42
编码为 0x54
(二进制 01010100
,LSB=0)。 这里的指针指的是广义的指针,表示栈变量、寄存器、对象属性等
• 堆对象:地址 0x1000
编码为 0x801
(二进制 100000000001
,LSB=1)。v8中地址是4/8字节对齐的(即地址是4/8的倍数,和机器字长有关),所以最低2/3位一定是0,右移一位不会导致1丢失
2. 数值范围
系统位数 | Smi 范围 | 堆对象数值范围 |
---|---|---|
32 位系统 | -2^30 到 2^30-1 (约 ±10 亿) | 所有无法用 Smi 表示的数(如浮点数、大整数) |
64 位系统 | -2^31 到 2^31-1 (约 ±20 亿) | 同上 |
示例
• Smi:2147483647
(32 位 Smi 上限)。
• 堆对象:3.14
(浮点数)、2147483648
(超出 Smi 范围的整数)。
3. 性能差异
操作 | Smi | 堆对象 |
---|---|---|
堆内存分配 | 无需分配(直接编码在指针中) | 需要堆分配(触发 GC 压力) |
数值运算 | 直接操作指针值(极快) | 需访问堆内存(较慢) |
类型检查 | 一次位操作(检查 LSB 是否为 0 ) | 需访问对象的隐藏类(Hidden Class) |
4. 使用场景
场景 | Smi | 堆对象 |
---|---|---|
高频小整数 | 循环计数器、临时计算结果、数组索引 | 不适用 |
大整数/浮点数 | 不适用 | 科学计算、大数运算、对象属性 |
动态类型转换 | 超出范围时自动转为 HeapNumber | 直接存储 |
隐式转换示例
let a = 42; // Smi
a = 3.14; // 自动转为 HeapNumber
a = 2147483648; // 超出 Smi 范围,转为 HeapNumber
5. 垃圾回收(GC)影响
特性 | Smi | 堆对象 |
---|---|---|
GC 扫描成本 | 无(不占用堆内存) | 需要跟踪和回收(增加 GC 压力) |
内存碎片 | 无 | 可能产生碎片(频繁分配/释放时) |
通过区分 Smi 和堆对象,V8 在保持 JavaScript 动态类型灵活性的同时,优化了高频操作的性能,减少了内存开销。
程序的整体执行流程如下
入口函数
主函数int main调用
v8::Shell::Main
int main(int argc, char* argv[]) { return v8::Shell::Main(argc, argv); }
Shell::Main调用Shell::RunMain
int Shell::Main(int argc, char* argv[]) {
// ...
result = RunMain(isolate, true);
// Shut down contexts and collect garbage.
return result;
}
Shell::RunMain调用Shell::RunMainIsolate
int Shell::RunMain(v8::Isolate* isolate, bool last_run) {
// ...
bool success = RunMainIsolate(isolate, keep_context_alive);
CollectGarbage(isolate);
// ...
return (success == Shell::options.expected_to_throw ? 1 : 0);
}
Shell::RunMainIsolate调用SourceGroup::Execute
bool Shell::RunMainIsolate(v8::Isolate* isolate, bool keep_context_alive) {
// ...
global_context.Get(isolate)->Enter();
if (!options.isolate_sources[0].Execute(isolate)) success = false;
global_context.Get(isolate)->Exit();
}
// ...
return success;
}
SourceGroup::Execute调用Shell::ExecuteString
bool SourceGroup::Execute(Isolate* isolate) {
bool success = true;
for (int i = begin_offset_; i < end_offset_; ++i) {
const char* arg = argv_[i];
// ...
HandleScope handle_scope(isolate);
// 从命令行参数中读取js文件名
Local<String> file_name =
String::NewFromUtf8(isolate, arg).ToLocalChecked();
Local<String> source;
// 读取文件内容,转化为v8字符串句柄
if (!Shell::ReadFile(isolate, arg).ToLocal(&source)) {
printf("Error reading '%s'\n", arg);
base::OS::ExitProcess(1);
}
Shell::set_script_executed();
Shell::update_script_size(source->Length());
// 执行js文件
if (!Shell::ExecuteString(isolate, source, file_name,
Shell::kReportExceptions)) {
success = false;
break;
}
}
return success;
}
Shell::ExecuteString调用Shell::CompileString进入编译
阶段,调用script->Run进入执行
阶段
bool Shell::ExecuteString(Isolate* isolate, Local<String> source,
Local<String> name,
ReportExceptions report_exceptions,
Global<Value>* out_result) {
// ...
Local<Script> script;
if (!CompileString<Script>(isolate, context, source, origin)
.ToLocal(&script)) {
return false;
}
// ...
MaybeLocal<Value> maybe_result = script->Run(realm);
return !try_catch.HasCaught();
}
编译
Shell::CompileString经过一系列调用,调用到CompileToplevel
MaybeHandle<SharedFunctionInfo> CompileToplevel(
ParseInfo* parse_info, Handle<Script> script,
MaybeDirectHandle<ScopeInfo> maybe_outer_scope_info, Isolate* isolate,
IsCompiledScope* is_compiled_scope) {
// 调用ParseProgram初始化parse_info->literal
if (parse_info->literal() == nullptr &&
!parsing::ParseProgram(parse_info, script, maybe_outer_scope_info,
isolate, parsing::ReportStatisticsMode::kYes)) {
FailWithException(isolate, script, parse_info,
Compiler::ClearExceptionFlag::KEEP_EXCEPTION);
return MaybeHandle<SharedFunctionInfo>();
}
// ...
Handle<SharedFunctionInfo> shared_info =
GetOrCreateTopLevelSharedFunctionInfo(parse_info, script, isolate,
is_compiled_scope);
// Prepare and execute compilation of the outer-most function.
if (!IterativelyExecuteAndFinalizeUnoptimizedCompilationJobs(
isolate, script, parse_info, isolate->allocator(), is_compiled_scope,
&finalize_unoptimized_compilation_data_list, nullptr)) {
FailWithException(isolate, script, parse_info,
Compiler::ClearExceptionFlag::KEEP_EXCEPTION);
return MaybeHandle<SharedFunctionInfo>();
}
// ...
return shared_info;
}
由源代码生成ast
CompileToplevel先生成了抽象语法树,存储在ParseInfo->literal中。
ParseInfo(解析阶段信息)
ParseInfo
存储 解析 JavaScript 源码生成抽象语法树(AST) 所需的全部信息。
核心字段及作用
字段名 | 类型 | 作用 |
---|---|---|
script_ | Handle<Script> | 关联的脚本对象,包含源码内容、文件名、行号等元数据。 |
literal_ | FunctionLiteral* | 解析生成的 函数字面量(AST根节点),描述函数的结构(参数、语句等)。 |
scope_ | Scope* | 当前作用域(全局/函数作用域),用于变量解析和静态作用域分析。 |
ast_value_factory_ | AstValueFactory* | 管理 AST 中字符串、数字等常量的内存分配(如标识符、字面量)。 |
flags_ | ParseFlags | 解析选项标志位(如是否严格模式、是否为箭头函数、是否允许尾调用优化等)。 |
reusable_state_ | ParserReusableState* | 解析器可复用的临时状态(如预解析缓存信息,加速重复解析)。 |
hash_seed_ | uint64_t | 哈希种子,用于确定化变量名哈希值(避免哈希碰撞攻击)。 |
extension_ | DeclarationScope* | 扩展作用域(如模块作用域或 eval 的动态作用域)。 |
stack_limit_ | uintptr_t | 递归解析时的堆栈深度限制,防止堆栈溢出。 |
ast
打印ast结构的命令如下
d8 --print-ast <filename>.js
1. 函数节点(FUNC at 0)
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
• at 0
:函数的起始位置(字节码偏移或源码位置)。
• KIND 0
:函数类型标识:
• 0
表示普通函数(如 function(){}
)。
• 其他值可能对应箭头函数(KIND 1
)、生成器(KIND 2
)等。
• LITERAL ID 0
:函数字面量在编译期的唯一标识符。
• SUSPEND COUNT 0
:生成器函数的中断次数(非生成器函数为 0)。
• NAME ""
:显式声明的函数名(此处为空,如匿名函数)。
• INFERRED NAME ""
:引擎推断的函数名(如通过 let foo = function(){}
推断为 "foo"
)。
2. 变量声明(DECLS)
. DECLS
. . VARIABLE (0000028BC18048F0) (mode = CONST, assigned = false) "smiVal"
. . VARIABLE (0000028BC18049B0) (mode = CONST, assigned = false) "heapNumberVal"
. . VARIABLE (0000028BC1804A70) (mode = CONST, assigned = false) "s"
. . VARIABLE (0000028BC1804B30) (mode = CONST, assigned = false) "obj"
• VARIABLE
:变量声明节点。
• (0000028BC18048F0)
:变量在内存中的唯一标识符(指针地址)。
• mode = CONST
:变量模式:
• CONST
:不可重新赋值(对应 const
声明)。
• LET
:块级作用域变量(对应 let
)。
• VAR
:函数作用域变量(对应 var
)。
• assigned = false
:是否已赋值(初始化表达式不算作赋值)。
• "smiVal"
:变量名。
3. 块节点(BLOCK NOCOMPLETIONS)
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 26
• BLOCK
:代码块节点,表示一个作用域块。
• NOCOMPLETIONS
:该块没有完成值(即没有 return
或表达式结果)。
• at -1
:块的位置信息未明确指定(可能为默认值)。
4. 表达式语句(EXPRESSION STATEMENT)
. . EXPRESSION STATEMENT at 26
. . . kInit at 26
. . . . VAR PROXY context[3] (0000028BC18048F0) (mode = CONST, assigned = false) "smiVal"
. . . . LITERAL 42
• EXPRESSION STATEMENT
:表达式语句节点。
• at 26
:对应源码的字符偏移位置(如第 26 个字符处)。
• kInit
:初始化类型:
• kInit
:变量初始化(如 const x = 42;
)。
• 其他类型:kAssignment
(赋值)、kCall
(函数调用)等。
• VAR PROXY
:变量代理节点,指向声明的变量。
• context[3]
:变量在作用域中的位置(第 3 个上下文槽)。
• (0000028BC18048F0)
:变量标识符(与 DECLS
中的地址对应)。
• LITERAL 42
:数值字面量(Small Integer,Smi)。
5. 字面量类型
(1) 数值字面量
. . . . LITERAL 42 // Smi(小整数)
. . . . LITERAL 3.14 // HeapNumber(堆分配的浮点数)
• LITERAL 42
:Smi(小整数),直接存储在指针中。
• LITERAL 3.14
:HeapNumber,堆内存中存储的浮点数。
(2) 字符串字面量
. . . . LITERAL "V8" // 字符串常量
• LITERAL "V8"
:字符串常量,存储在常量池或堆中。
(3) 对象字面量
. . . . OBJ LITERAL at 255
. . . . . PROPERTY - CONSTANT
. . . . . . KEY at 257
. . . . . . . LITERAL "x"
. . . . . . VALUE at 260
. . . . . . . LITERAL 1
• OBJ LITERAL
:对象字面量节点。
• PROPERTY - CONSTANT
:属性类型为常量(不可变)。
• KEY at 257
:属性键的位置(字符偏移 257)。
• VALUE at 260
:属性值的的位置(字符偏移 260)。
• LITERAL "x"
:属性键为字符串 "x"
。
• LITERAL 1
:属性值为数值 1
。
6. 上下文(context)
• VAR PROXY context[3]
:变量存储在作用域链的第 3 个上下文槽中。
• 上下文槽分配:
• context[0]
:当前函数的作用域。
• context[1]
:闭包变量或外层作用域。
• context[3]
:当前作用域的局部变量。
总结
字段 | 含义 |
---|---|
FUNC | 函数节点,描述函数的类型、名称和作用域。 |
DECLS | 变量声明列表,记录变量名、作用域位置和类型。 |
BLOCK | 代码块节点,表示一个作用域或语句块。 |
EXPRESSION STATEMENT | 表达式语句节点,如变量初始化或赋值。 |
VAR PROXY | 变量代理节点,关联变量声明和实际使用。 |
LITERAL | 字面量节点,表示常量值(数值、字符串、对象等)。 |
context[N] | 变量在作用域链中的存储位置,用于代码生成阶段定位变量。 |
ast的代码实现
重点关注ParseInfo的literal字段,该字段描述了抽象语法树,初始化的核心流程在Parser::DoParseProgram。
FunctionLiteral* Parser::DoParseProgram(Isolate* isolate, ParseInfo* info) {
// ...
ParseStatementList(&body, Token::kEos);
// ...
int parameter_count = 0;
result = factory()->NewScriptOrEvalFunctionLiteral(
scope, body, function_state.expected_property_count(), parameter_count);
result->set_suspend_count(function_state.suspend_count());
// ...
return result;
}
ParseStatementList会解析顶级作用域里的所有语句,构建astNode,astNode会记录语句在源码的起始位置以及长度,astNode的派生类会记录不同语句的额外信息。
至于需要构建什么语句,则需要根据下一个token来决定。核心逻辑在ParserBase::ParseStatementListItem
template <typename Impl>
typename ParserBase<Impl>::StatementT
ParserBase<Impl>::ParseStatementListItem() {
switch (peek()) {
case Token::kFunction:
return ParseHoistableDeclaration(nullptr, false);
case Token::kClass:
Consume(Token::kClass);
return ParseClassDeclaration(nullptr, false);
case Token::kVar:
case Token::kConst:
return ParseVariableStatement(kStatementListItem, nullptr);
case Token::kLet:
if (IsNextLetKeyword()) {
return ParseVariableStatement(kStatementListItem, nullptr);
}
break;
case Token::kUsing:
if (!v8_flags.js_explicit_resource_management) break;
if (!is_using_allowed()) break;
if (!(scanner()->HasLineTerminatorAfterNext()) &&
Token::IsAnyIdentifier(PeekAhead())) {
return ParseVariableStatement(kStatementListItem, nullptr);
}
break;
case Token::kAwait:
if (!v8_flags.js_explicit_resource_management) break;
if (!is_await_allowed()) break;
if (!is_using_allowed()) break;
if (!(scanner()->HasLineTerminatorAfterNext()) &&
PeekAhead() == Token::kUsing &&
!(scanner()->HasLineTerminatorAfterNextNext()) &&
Token::IsAnyIdentifier(PeekAheadAhead())) {
return ParseVariableStatement(kStatementListItem, nullptr);
}
break;
case Token::kAsync:
if (PeekAhead() == Token::kFunction &&
!scanner()->HasLineTerminatorAfterNext()) {
Consume(Token::kAsync);
return ParseAsyncFunctionDeclaration(nullptr, false);
}
break;
default:
break;
}
return ParseStatement(nullptr, nullptr, kAllowLabelledFunctionStatement);
}
然后,CompileToplevel生成了CompileToplevel。
在 V8 引擎中,Handle<SharedFunctionInfo> shared_info
的作用是 管理顶层代码的编译元信息,具体如下:
1. SharedFunctionInfo
的核心作用
• 存储函数元信息:
SharedFunctionInfo
(SFI)是 V8 内部用于存储 函数元信息 的核心数据结构,包括:
• 源代码位置(起点、长度)。
• 函数名、参数数量、预期作用域。
• 编译后的代码指针(Code
对象)。
• 优化状态(是否经过 TurboFan 优化)。
• 共享与复用:
SFI 是 跨执行上下文共享 的,即使多次执行同一函数,也只需编译一次,提升性能。
2. GetOrCreateTopLevelSharedFunctionInfo
的作用
此函数用于 获取或创建顶层代码的 SFI,具体逻辑如下:
- 检查是否已存在:
如果脚本已编译过,直接返回现有的 SFI(避免重复编译)。 - 创建新的 SFI:
如果不存在,初始化新的 SFI 并关联到当前脚本。 - 关联编译信息:
将解析阶段生成的元信息(如 AST、作用域链)绑定到 SFI。
3. shared_info
在编译流程中的角色
• 编译入口:
SFI 是编译过程的起点,V8 通过它获取代码的元信息,并生成可执行的字节码或机器码。
• 代码缓存:
编译后的代码(如 Ignition 字节码或 TurboFan 优化代码)存储在 SFI 中,后续执行直接复用。
• 优化触发:
SFI 跟踪函数的热度(执行频率),当达到阈值时触发 TurboFan 优化。
接着,CompileToplevel调用IterativelyExecuteAndFinalizeUnoptimizedCompilationJobs。
template <typename IsolateT>
bool IterativelyExecuteAndFinalizeUnoptimizedCompilationJobs(
IsolateT* isolate, Handle<Script> script, ParseInfo* parse_info,
AccountingAllocator* allocator, IsCompiledScope* is_compiled_scope,
FinalizeUnoptimizedCompilationDataList*
finalize_unoptimized_compilation_data_list,
DeferredFinalizationJobDataList*
jobs_to_retry_finalization_on_main_thread) {
// ...
std::unique_ptr<UnoptimizedCompilationJob> job =
ExecuteSingleUnoptimizedCompilationJob(parse_info, literal, script,
allocator, &functions_to_compile,
isolate->AsLocalIsolate());
// ...
return compilation_succeeded;
}
由ast生成字节码
打印字节码指令
d8 --print-bytecode <filename>.js
执行结果如下
Frame
栈帧用于存储函数的局部变量、参数和临时数据。
若函数有多个局部变量或复杂表达式,栈帧大小会相应增加。
000001EA00140048 @ 0 : 0d 2a LdaSmi [42]
-
地址 000001EA00140048:
当前字节码指令在内存中的地址(具体值因运行环境而异)。 -
@ 0:
指令在字节码数组中的 偏移量(offset) 为 0,表示这是函数的第一条指令。 -
操作码 0d 2a:
0d 是操作码(opcode)的十六进制值,对应 LdaSmi 指令。
2a 是操作数(operand)的十六进制值,对应十进制值 42。 -
指令 LdaSmi [42]:
作用:将小整数(Smi)42 加载到累加器(Accumulator)。
Constant pool
除了smi外,其他常量需要被分配堆对象。
TrustedFixedArray:
V8 内部用于存储 不可变数据 的数组结构,通常用于常量池或元数据存储。
Map 对象(也称为 Hidden Class)
是 V8 内部用于描述 对象内存布局 的元数据,包含以下信息:
- 对象类型(如 TRUSTED_FIXED_ARRAY_TYPE)。
- 属性数量和存储偏移量。
- 元素类型(如 Smi、HeapObject 等)。
- 继承关系和原型链。
作用:
- 优化属性访问速度(通过预计算偏移量)。
- 支持动态类型追踪(Type Feedback),为 JIT 编译提供信息。
Handler Table
作用:存储 异常处理逻辑(如 try/catch 的跳转地址)。
当前状态:size=0 表示当前代码无异常处理逻辑。
Source Position Table (size = 0)
作用:映射 字节码偏移量 到 源代码行号,用于调试和错误堆栈。
当前状态:size=0 可能表示未启用调试符号生成或代码无复杂控制流。
具体实现
IterativelyExecuteAndFinalizeUnoptimizedCompilationJobs经过一系列调用,会调用到GenerateBodyStatementsWithoutImplicitFinalReturn。
void BytecodeGenerator::GenerateBodyStatementsWithoutImplicitFinalReturn(
int start) {
ZonePtrList<Statement>* body = info()->literal()->body();
// ...
VisitStatements(body, start);
}
info()->literal()->body()获取到的就是之前生成的astNode,VisitStatements会遍历每个statement并进行访问,并根据statement的类型生成不同的字节码。
在 V8 引擎中,JavaScript 代码被解析为 抽象语法树(AST),所有语法结构均以不同类型的 Statement
(语句)和 Expression
(表达式)节点表示。以下是 deepseek给出的V8 中常见的 Statement
和 Expression
类型,最新类型请以ast/ast.h为准。
一、Statement(语句)
语句表示代码的执行单元,通常不直接返回值,而是控制程序流程或执行操作。
1. 基本语句
类型 | 作用 | 示例 |
---|---|---|
BlockStatement | 代码块,包含多个语句(用 {} 包裹) | { let x = 1; console.log(x); } |
EmptyStatement | 空语句 | ; (仅分号) |
ExpressionStatement | 表达式语句,将表达式作为语句执行 | x = 1; 或 foo(); |
DebuggerStatement | 调试语句(触发调试器断点) | debugger; |
2. 变量与声明
类型 | 作用 | 示例 |
---|---|---|
VariableDeclaration | 变量声明(var /let /const ) | const x = 1; |
FunctionDeclaration | 函数声明 | function foo() {} |
ClassDeclaration | 类声明 | class Bar {} |
3. 控制流语句
类型 | 作用 | 示例 |
---|---|---|
IfStatement | 条件语句(if /else ) | if (x > 0) { ... } else { ... } |
SwitchStatement | 多分支条件语句 | switch (x) { case 1: ... } |
ForStatement | for 循环 | for (let i=0; i<10; i++) { ... } |
ForInStatement | for...in 循环 | for (let key in obj) { ... } |
ForOfStatement | for...of 循环(遍历可迭代对象) | for (let item of arr) { ... } |
WhileStatement | while 循环 | while (x > 0) { ... } |
DoWhileStatement | do...while 循环 | do { ... } while (x > 0); |
BreakStatement | 跳出循环或 switch | break; |
ContinueStatement | 跳过当前循环迭代 | continue; |
4. 函数与模块
类型 | 作用 | 示例 |
---|---|---|
ReturnStatement | 函数返回语句 | return x; |
ThrowStatement | 抛出异常 | throw new Error(); |
TryStatement | try/catch/finally 异常处理 | try { ... } catch (e) { ... } |
ImportDeclaration | 导入模块(ES6 import ) | import { x } from 'mod'; |
ExportDeclaration | 导出模块(ES6 export ) | export const x = 1; |
二、Expression(表达式)
表达式会返回一个值,可以作为其他表达式或语句的一部分。
1. 基本表达式
类型 | 作用 | 示例 |
---|---|---|
Literal | 字面量(数值、字符串、布尔值、null 、undefined ) | 42 , "hello" , true , null |
Identifier | 标识符(变量名、属性名) | x , console.log |
ThisExpression | this 关键字 | this.value |
Super | super 关键字(类继承中访问父类方法) | super.method() |
2. 对象与数组
类型 | 作用 | 示例 |
---|---|---|
ObjectExpression | 对象字面量 | { x: 1, y: 2 } |
ArrayExpression | 数组字面量 | [1, 2, 3] |
SpreadElement | 展开运算符(... ) | [...arr, 4] |
3. 运算表达式
类型 | 作用 | 示例 |
---|---|---|
AssignmentExpression | 赋值操作(= 、+= 等) | x = 1 或 x += 2 |
BinaryExpression | 二元运算(算术、逻辑、比较等) | x + y , a > b , x && y |
UnaryExpression | 一元运算(! 、- 、typeof 等) | -x , typeof x , delete obj.x |
UpdateExpression | 自增/自减(++ 、-- ) | x++ 或 --y |
LogicalExpression | 逻辑运算(&& 、` |
4. 函数与调用
类型 | 作用 | 示例 |
---|---|---|
CallExpression | 函数调用 | foo() , obj.method() |
NewExpression | new 运算符创建实例 | new MyClass() |
ArrowFunctionExpression | 箭头函数 | (x) => x + 1 |
FunctionExpression | 函数表达式 | function () {} |
5. 高级表达式
类型 | 作用 | 示例 |
---|---|---|
MemberExpression | 访问对象属性或方法 | obj.x 或 arr[0] |
ConditionalExpression | 三元条件运算符(? : ) | x > 0 ? 'yes' : 'no' |
TemplateLiteral | 模板字符串(反引号包裹) | `Value: ${x}` |
TaggedTemplateExpression | 标签模板字符串 | tag`Hello ${name}` |
YieldExpression | yield 关键字(生成器函数) | yield 1; |
AwaitExpression | await 关键字(异步函数) | await fetch(url); |
三、V8 内部优化
在 V8 的编译过程中,AST 节点会被转换为 字节码 或直接优化为 机器码。以下是一些关键优化点:
- 隐藏类(Hidden Class):对象字面量(
ObjectExpression
)会生成隐藏类以加速属性访问。 - 内联缓存(Inline Cache):高频调用的
CallExpression
(如obj.method()
)会被缓存方法地址。 - 类型反馈(Type Feedback):
BinaryExpression
和AssignmentExpression
会收集类型信息以优化运算。
四、示例代码的 AST 映射
对于以下代码:
function sum(a, b) {
return a + b;
}
const result = sum(1, 2);
其 AST 结构为:
• FunctionDeclaration(sum
函数)
• Identifier(a
、b
参数)
• BlockStatement(函数体)
◦ ReturnStatement
◦ BinaryExpression(a + b
)
• VariableDeclaration(const result
)
• CallExpression(sum(1, 2)
)
◦ Literal(1
、2
)
对于字面量而言,还需要区分smi和其他字面量,因为smi不需要被加入常量池,这段处理逻辑在LoadLiteral。
BytecodeArrayBuilder& BytecodeArrayBuilder::LoadLiteral(double value) {
// If we can encode the value as a Smi, we should.
int smi;
if (DoubleToSmiInteger(value, &smi)) {
LoadLiteral(Smi::FromInt(smi));
} else {
size_t entry = GetConstantPoolEntry(value);
OutputLdaConstant(entry);
}
return *this;
}
DoubleToSmiInteger的逻辑就是调用IsSmiDouble判断是否为smi,如果是就将其由double类型转为Int类型。
IsSmiDouble就是按value是否在smi大小范围内来判断的。
bool IsSmiDouble(double value) {
return value >= Smi::kMinValue && value <= Smi::kMaxValue &&
!IsMinusZero(value) && value == FastI2D(FastD2I(value));
}
GetConstantPoolEntry方法会把字面量加入常量池,核心逻辑在 ConstantArrayBuilder::ConstantArraySlice::Allocate。
size_t ConstantArrayBuilder::ConstantArraySlice::Allocate(
ConstantArrayBuilder::Entry entry, size_t count) {
DCHECK_GE(available(), count);
size_t index = constants_.size();
DCHECK_LT(index, capacity());
for (size_t i = 0; i < count; ++i) {
// constants_用来记录哪些字面量需要加入常量池
constants_.push_back(entry);
}
return index + start_index();
}
字面量在constants_中的下标也会被放入heap_xx_map,heap_xx_map会在预分配堆对象
阶段使用。
size_t ConstantArrayBuilder::Insert(double number) {
if (std::isnan(number)) return InsertNaN();
auto entry = heap_number_map_.find(number);
if (entry == heap_number_map_.end()) {
index_t index = static_cast<index_t>(AllocateIndex(Entry(number)));
heap_number_map_[number] = index;
return index;
}
return entry->second;
}
对于object对象,会调用AllocateDeferredConstantPoolEntry将模板加入常量池。
Handle<BytecodeArray> BytecodeGenerator::FinalizeBytecode(
IsolateT* isolate, Handle<Script> script) {
// ...
Handle<BytecodeArray> bytecode_array = builder()->ToBytecodeArray(isolate);
// ...
return bytecode_array;
}
当执行const converted = Number(smiVal)
时,该表达式会被识别为GlobalCall,由于Number被注册为js关键字,所以会调用LoadGlobal函数。LoadGlobal最终也会调用到Allocate函数,将Number
这个函数名作为字符串
加入常量池。
void BytecodeGenerator::BuildVariableLoad(Variable* variable,
HoleCheckMode hole_check_mode,
TypeofMode typeof_mode) {
switch (variable->location()) {
// ...
case VariableLocation::UNALLOCATED: {
if (variable->raw_name() == ast_string_constants()->undefined_string()) {
builder()->LoadUndefined();
} else {
FeedbackSlot slot = GetCachedLoadGlobalICSlot(typeof_mode, variable);
builder()->LoadGlobal(variable->raw_name(), feedback_index(slot),
typeof_mode);
}
break;
}
// ...
}
}
最终ToBytecodeArray方法存储生成的字节码,该方法会调用ToFixedArray方法创建常量池。
Handle<BytecodeArray> BytecodeArrayWriter::ToBytecodeArray(
// ...
int bytecode_size = static_cast<int>(bytecodes()->size());
// 栈帧长度 = 需要保存的寄存器数量 * 指针长度
int frame_size = register_count * kSystemPointerSize;
// 初始化常量池,预分配堆变量可以提升执行效率
DirectHandle<TrustedFixedArray> constant_pool =
constant_array_builder()->ToFixedArray(isolate);
Handle<BytecodeArray> bytecode_array = isolate->factory()->NewBytecodeArray(
bytecode_size, &bytecodes()->front(), frame_size, parameter_count,
max_arguments, constant_pool, handler_table);
return bytecode_array;
}
ToFixedArray方法会便利constants_,调用ToHandle方法,ToHandle方法根据constant的tag_属性,创建不同类型的堆变量。
template <typename IsolateT>
Handle<Object> ConstantArrayBuilder::Entry::ToHandle(IsolateT* isolate) const {
switch (tag_) {
case Tag::kDeferred:
// We shouldn't have any deferred entries by now.
UNREACHABLE();
case Tag::kHandle:
return handle_;
case Tag::kSmi:
case Tag::kJumpTableSmi:
return handle(smi_, isolate);
case Tag::kUninitializedJumpTableSmi:
// TODO(leszeks): There's probably a better value we could use here.
return isolate->factory()->the_hole_value();
case Tag::kRawString:
return raw_string_->string();
case Tag::kConsString:
return cons_string_->AllocateFlat(isolate);
case Tag::kHeapNumber:
return isolate->factory()->template NewNumber<AllocationType::kOld>(
heap_number_);
case Tag::kBigInt:
// This should never fail: the parser will never create a BigInt
// literal that cannot be allocated.
return BigIntLiteral(isolate, bigint_.c_str()).ToHandleChecked();
case Tag::kScope:
return scope_->scope_info();
#define ENTRY_LOOKUP(Name, name) \
case Tag::k##Name: \
return isolate->factory()->name();
SINGLETON_CONSTANT_ENTRY_TYPES(ENTRY_LOOKUP);
#undef ENTRY_LOOKUP
}
UNREACHABLE();
}
自此,编译阶段结束,进入执行阶段。
执行
我们在js文件中调用的%DebugPrint函数最终会调用到DebugPrintImpl。该函数先获取到smi或者堆对象,打印出来,如果是堆对象还会额外打印map信息。
static void DebugPrintImpl(Tagged<MaybeObject> maybe_object, std::ostream& os) {
// ...
Tagged<Object> object = maybe_object.GetHeapObjectOrSmi();
os << "DebugPrint: ";
Print(object, os);
if (IsHeapObject(object)) {
Print(Cast<HeapObject>(object)->map(), os);
}
// ...
os << std::endl;
}
根据指针最末尾是否为0来判断是否是smi,如果是,就直接返回指针(因为smi值存在指针中),如果不是,则获取堆对象地址(即指针左移一位再-1)
Tagged<Object> TaggedImpl<kRefType, StorageType>::GetHeapObjectOrSmi() const {
CHECK(kIsFull);
if (IsSmi()) {
return Tagged<Object>(ptr_);
}
return GetHeapObject();
}
constexpr bool IsSmi() const { return HAS_SMI_TAG(ptr_); }
#define HAS_SMI_TAG(value) \
((static_cast<i::Tagged_t>(value) & ::i::kSmiTagMask) == ::i::kSmiTag)
template <HeapObjectReferenceType kRefType, typename StorageType>
Tagged<HeapObject> TaggedImpl<kRefType, StorageType>::GetHeapObject() const {
// ...
return Cast<HeapObject>(Tagged<Object>(ptr_));
}
打印的具体实现。
Smi::ToInt会将指针右移一位,还原成存储的真实值
void Print(Tagged<Object> obj, std::ostream& os) {
if (IsSmi(obj)) {
os << "Smi: " << std::hex << "0x" << Smi::ToInt(obj);
os << std::dec << " (" << Smi::ToInt(obj) << ")\n";
} else {
Cast<HeapObject>(obj)->HeapObjectPrint(os);
}
}
void HeapNumber::HeapNumberPrint(std::ostream& os) {
PrintHeader(os, "HeapNumber");
os << "\n - value: ";
HeapNumberShortPrint(os);
os << "\n";
}
void HeapObject::PrintHeader(std::ostream& os, const char* id) {
PrintHeapObjectHeaderWithoutMap(*this, os, id);
// 获取堆的基地址,堆对象真实地址 = 基地址 + 压缩指针地址
PtrComprCageBase cage_base = GetPtrComprCageBase();
if (!SafeEquals(GetReadOnlyRoots().meta_map())) {
os << "\n - map: " << Brief(map(cage_base));
}
}
void PrintHeapObjectHeaderWithoutMap(Tagged<HeapObject> object,
std::ostream& os, const char* id) {
os << reinterpret_cast<void*>(object.ptr()) << ": [";
os << id;
os << "]";
}
void HeapNumber::HeapNumberShortPrint(std::ostream& os) {
double val = value();
if (i::IsMinusZero(val)) {
os << "-0.0";
} else if (val == DoubleToInteger(val) && val >= kMinSafeInteger &&
val <= kMaxSafeInteger) {
// Print integer HeapNumbers in safe integer range with max precision: as
// 9007199254740991.0 instead of 9.0072e+15
int64_t i = static_cast<int64_t>(val);
os << i << ".0";
} else {
os << val;
}
}
对于Number()构造函数而言,如果传入的字面量是smi,那么Number函数会直接返回Smi对象,否则返回堆对象。
相关类型转换可以在convert.tq中找到。Torque 是 V8 引擎团队专门设计的一种领域特定语言(Domain-Specific Language, DSL),用于高效、安全地实现 V8 的 内置函数(Builtins) 和 底层运行时逻辑。它的核心目标是简化 V8 内部代码的开发和维护,同时生成高性能的机器代码。torque入门指南
Convert<Number, int32>(i: int32): Number {
return ChangeInt32ToTagged(i);
}
TNode<Number> CodeStubAssembler::ChangeInt32ToTagged(TNode<Int32T> value) {
if (SmiValuesAre32Bits()) {
return SmiTag(ChangeInt32ToIntPtr(value));
}
// ...
{
TNode<Float64T> value64 = ChangeInt32ToFloat64(value);
TNode<HeapNumber> result = AllocateHeapNumberWithValue(value64);
var_result = result;
Goto(&if_join);
}
// ...
return var_result.value();
}