共计 8947 个字符,预计需要花费 23 分钟才能阅读完成。
简介:本文是一个 V8 编译原理常识的介绍文章,旨在让大家理性的理解 JavaScript 在 V8 中的解析过程。
作者 | 子弈
起源 | 阿里技术公众号
一 简介
本文是一个 V8 编译原理常识的介绍文章,旨在让大家理性的理解 JavaScript 在 V8 中的解析过程。本文次要的撰写流程如下:
- 解释器和编译器:计算机编译原理的基础知识介绍
- V8 的编译原理:基于计算机编译原理的常识,理解 V8 对于 JavaScript 的解析流程
- V8 的运行时体现:联合 V8 的编译原理,实际 V8 在解析流程中的具体运行体现
本文仅代表个人观点,文中若有谬误欢送斧正。
二 解释器和编译器
大家可能始终纳闷的问题:JavaScript 是一门解释型语言吗?要理解这个问题,首先须要初步理解什么是解释器和编译器以及它们的特点是什么。
1 解释器
解释器的作用是将某种语言编写的源程序作为输出,将该源程序执行的后果作为输入,例如 Perl、Scheme、APL 等都是应用解释器进行转换执行:
2 编译器
编译器的设计是一个十分宏大和简单的软件系统设计,在真正设计的时候须要解决两个绝对重要的问题:
- 如何剖析不同高级程序语言设计的源程序
- 如何将源程序的性能等价映射到不同指令系统的指标机器
两头示意(IR)
两头示意(Intermediate Representation,IR)是程序结构的一种体现形式,它会比形象语法树(Abstract Syntax Tree,AST)更加靠近汇编语言或者指令集,同时也会保留源程序中的一些高级信息,具体作用包含:
- 易于编译器的谬误调试,容易辨认是 IR 之前的前端还是之后的后端出的问题
- 能够使得编译器的职责更加拆散,源程序的编译更多关注如何转换成 IR,而不是去适配不同的指令集
- IR 更加靠近指令集,从而绝对于源码能够更加节俭内存空间
优化编译器
IR 自身能够做到多趟迭代从而优化源程序,在每一趟迭代的过程中能够钻研代码并记录优化的细节,不便后续的迭代查找并利用这些优化信息,最终能够高效输入更优的目标程序:
优化器能够对 IR 进行一趟或者多趟解决,从而生成更快执行速度或者更小体积的目标程序(例如找到循环中不变的计算并对其进行优化从而缩小运算次数),也可能用于产生更少异样或者更低功耗的目标程序。除此之外,前端和后端外部还能够细分为多个解决步骤,具体如下图所示:
3 两者的个性比拟
解释器和编译器的具体个性比拟如下所示:
须要留神晚期的 Web 前端要求页面的启动速度快,因而采纳解释执行的形式,然而页面在运行的过程中性能绝对较低。为了解决这个问题,须要在运行时对 JavaScript 代码进行优化,因而在 JavaScript 的解析引擎中引入了 JIT 技术。
4 JIT 编译技术
JIT(Just In Time)编译器是一种动静编译技术,绝对于传统编译器而言,最大的区别在于编译时和运行时不拆散,是一种在运行的过程中对代码进行动静编译的技术。
5 混合动静编译技术
为了解决 JavaScript 在运行时性能较慢的问题,能够通过引入 JIT 技术,并采纳混合动静编译的形式来晋升 JavaScript 的运行性能,具体思路如下所示:
采纳上述编译框架后,能够使得 JavaScript 语言:
- 启动速度快:在 JavaScript 启动的时候采纳解释执行的形式运行,利用了解释器启动速度快的个性
- 运行性能高:在 JavaScript 运行的过程中能够对代码进行监控,从而应用 JIT 技术对代码进行编译优化
三 V8 的编译原理
V8 是一个开源的 JavaScript 虚拟机,目前次要用在 Chrome 浏览器(包含开源的 Chromium)以及 Node.js 中,外围性能是用于解析和执行 JavaScript 语言。为了解决晚期 JavaScript 运行性能差的问题,V8 经验了多个历史的编译框架衍变之后(感兴趣的同学能够理解一下晚期的 V8 编译框架设计),引入混合动静编译的技术来解决问题,具体具体的编译框架如下所示:
1 Ignition 解释器
Ignition 的次要作用是将 AST 转换成 Bytecode(字节码,两头示意)。在运行的过程中,还会应用类型反馈(TypeFeedback)技术并计算热点代码(HotSpot,反复被运行的代码,能够是办法也能够是循环体),最终交给 TurboFan 进行动静运行时的编译优化。Ignition 的解释执行流程如下所示:
在字节码解释执行的过程中,会将须要进行性能优化的运行时信息指向对应的 Feedback Vector(反馈向量,之前也被称为 Type Feedback Vector),Feeback Vector 中会蕴含依据内联缓存(Inline Cache,IC)来存储的多种类型的插槽(Feedback Vector Slot)信息,例如 BinaryOp 插槽(二进制操作后果的数据类型)、Invocation Count(函数的调用次数)以及 Optimized Code 信息等。
这里不会过多解说每个执行流程的细节问题。
2 TurboFan 优化编译器
TurboFan 利用了 JIT 编译技术,次要作用是对 JavaScript 代码进行运行时编译优化,具体的流程如下所示:
图片出处 An Introduction to Speculative Optimization in V8。
须要留神 Profiling Feedback 局部,这里次要提供 Ignition 解释执行过程中生成的运行时反馈向量信息 Feedback Vector,Turbofan 会联合字节码以及反馈向量信息生成图示(数据结构中的图构造),并将图传递给前端局部,之后会依据反馈向量信息对代码进行优化和去优化。
这里的去优化是指让代码回退到 Ignition 进行解释执行,去优化实质是因为机器码曾经不能满足运行诉求,例如一个变量从 string 类型转变成 number 类型,机器码编译的是 string 类型,此时曾经无奈再满足运行诉求,因而 V8 会执行去优化动作,将代码回退到 Ignition 进行解释执行。
四 V8 的运行时体现
在理解 V8 的编译原理之后,接下来须要应用 V8 的调试工具来具体查看 JavaScript 的编译和运行信息,从而加深咱们对 V8 的编译过程认知。
1 D8 调试工具
如果想理解 JavaScript 在 V8 中的编译时和运行时信息,能够应用调试工具 D8。D8 是 V8 引擎的命令行 Shell,能够查看 AST 生成、中间代码 ByteCode、优化代码、反优化代码、优化编译器的统计数据、代码的 GC 等信息。D8 的装置形式有很多,如下所示:
- 办法一:依据 V8 官网文档 Using d8 以及 Building V8 with GN 进行工具链的下载和编译
- 办法二:应用他人曾经编译好的 D8 工具,可能版本会有滞后性,例如 Mac 版
- 办法三:应用 JavaScript 引擎版本管理工具,例如 jsvu,能够下载到最新编译好的 JavaScript 引擎
本文应用办法三装置 v8-debug 工具,装置实现后执行 v8-debug –help 能够查看有哪些命令:
# 执行 help 命令查看反对的参数
v8-debug --help
Synopsis:
shell [options] [--shell] [<file>...]
d8 [options] [-e <string>] [--shell] [[--module|--web-snapshot] <file>...]
-e execute a string in V8
--shell run an interactive JavaScript shell
--module execute a file as a JavaScript module
--web-snapshot execute a file as a web snapshot
SSE3=1 SSSE3=1 SSE4_1=1 SSE4_2=1 SAHF=1 AVX=1 AVX2=1 FMA3=1 BMI1=1 BMI2=1 LZCNT=1 POPCNT=1 ATOM=0
The following syntax for options is accepted (both '-' and '--' are ok):
--flag (bool flags only)
--no-flag (bool flags only)
--flag=value (non-bool flags only, no spaces around '=')
--flag value (non-bool flags only)
-- (captures all remaining args in JavaScript)
Options:
# 打印生成的字节码
--print-bytecode (print bytecode generated by ignition interpreter)
type: bool default: --noprint-bytecode
# 跟踪被优化的信息
--trace-opt (trace optimized compilation)
type: bool default: --notrace-opt
--trace-opt-verbose (extra verbose optimized compilation tracing)
type: bool default: --notrace-opt-verbose
--trace-opt-stats (trace optimized compilation statistics)
type: bool default: --notrace-opt-stats
# 跟踪去优化的信息
--trace-deopt (trace deoptimization)
type: bool default: --notrace-deopt
--log-deopt (log deoptimization)
type: bool default: --nolog-deopt
--trace-deopt-verbose (extra verbose deoptimization tracing)
type: bool default: --notrace-deopt-verbose
--print-deopt-stress (print number of possible deopt points)
# 查看编译生成的 AST
--print-ast (print source AST)
type: bool default: --noprint-ast
# 查看编译生成的代码
--print-code (print generated code)
type: bool default: --noprint-code
# 查看优化后的代码
--print-opt-code (print optimized code)
type: bool default: --noprint-opt-code
# 容许在源代码中应用 V8 提供的原生 API 语法
--allow-natives-syntax (allow natives syntax)
type: bool default: --noallow-natives-syntax
2 生成 AST
咱们编写一个 index.js 文件,在文件中写入 JavaScript 代码,执行一个简略的 add 函数:
function add(x, y) {return x + y}
console.log(add(1, 2));
应用 –print-ast 参数能够打印 add 函数的 AST 信息:
v8-debug --print-ast ./index.js
[generating bytecode for function:]
--- AST ---
FUNC at 0
. KIND 0
. LITERAL ID 0
. SUSPEND COUNT 0
. NAME "". INFERRED NAME""
. DECLS
. . FUNCTION "add" = function add
. EXPRESSION STATEMENT at 41
. . ASSIGN at -1
. . . VAR PROXY local[0] (0x7fb8c080e630) (mode = TEMPORARY, assigned = true) ".result"
. . . CALL
. . . . PROPERTY at 49
. . . . . VAR PROXY unallocated (0x7fb8c080e6f0) (mode = DYNAMIC_GLOBAL, assigned = false) "console"
. . . . . NAME log
. . . . CALL
. . . . . VAR PROXY unallocated (0x7fb8c080e470) (mode = VAR, assigned = true) "add"
. . . . . LITERAL 1
. . . . . LITERAL 2
. RETURN at -1
. . VAR PROXY local[0] (0x7fb8c080e630) (mode = TEMPORARY, assigned = true) ".result"
[generating bytecode for function: add]
--- AST ---
FUNC at 12
. KIND 0
. LITERAL ID 1
. SUSPEND COUNT 0
. NAME "add"
. PARAMS
. . VAR (0x7fb8c080e4d8) (mode = VAR, assigned = false) "x"
. . VAR (0x7fb8c080e580) (mode = VAR, assigned = false) "y"
. DECLS
. . VARIABLE (0x7fb8c080e4d8) (mode = VAR, assigned = false) "x"
. . VARIABLE (0x7fb8c080e580) (mode = VAR, assigned = false) "y"
. RETURN at 25
. . ADD at 34
. . . VAR PROXY parameter[0] (0x7fb8c080e4d8) (mode = VAR, assigned = false) "x"
. . . VAR PROXY parameter[1] (0x7fb8c080e580) (mode = VAR, assigned = false) "y"
咱们以图形化的形式来形容生成的 AST 树:
VAR PROXY 节点在真正的分析阶段会连贯到对应地址的 VAR 节点。
3 生成字节码
AST 会通过 Ignition 解释器的 BytecodeGenerator 函数生成字节码(两头示意),咱们能够通过 –print-bytecode 参数来打印字节码信息:
v8-debug --print-bytecode ./index.js
[generated bytecode for function: (0x3ab2082933f5 <SharedFunctionInfo>)]
Bytecode length: 43
Parameter count 1
Register count 6
Frame size 48
OSR nesting level: 0
Bytecode Age: 0
0x3ab2082934be @ 0 : 13 00 LdaConstant [0]
0x3ab2082934c0 @ 2 : c3 Star1
0x3ab2082934c1 @ 3 : 19 fe f8 Mov <closure>, r2
0x3ab2082934c4 @ 6 : 65 52 01 f9 02 CallRuntime [DeclareGlobals], r1-r2
0x3ab2082934c9 @ 11 : 21 01 00 LdaGlobal [1], [0]
0x3ab2082934cc @ 14 : c2 Star2
0x3ab2082934cd @ 15 : 2d f8 02 02 LdaNamedProperty r2, [2], [2]
0x3ab2082934d1 @ 19 : c3 Star1
0x3ab2082934d2 @ 20 : 21 03 04 LdaGlobal [3], [4]
0x3ab2082934d5 @ 23 : c1 Star3
0x3ab2082934d6 @ 24 : 0d 01 LdaSmi [1]
0x3ab2082934d8 @ 26 : c0 Star4
0x3ab2082934d9 @ 27 : 0d 02 LdaSmi [2]
0x3ab2082934db @ 29 : bf Star5
0x3ab2082934dc @ 30 : 63 f7 f6 f5 06 CallUndefinedReceiver2 r3, r4, r5, [6]
0x3ab2082934e1 @ 35 : c1 Star3
0x3ab2082934e2 @ 36 : 5e f9 f8 f7 08 CallProperty1 r1, r2, r3, [8]
0x3ab2082934e7 @ 41 : c4 Star0
0x3ab2082934e8 @ 42 : a9 Return
Constant pool (size = 4)
0x3ab208293485: [FixedArray] in OldSpace
- map: 0x3ab208002205 <Map>
- length: 4
0: 0x3ab20829343d <FixedArray[2]>
1: 0x3ab208202741 <String[7]: #console>
2: 0x3ab20820278d <String[3]: #log>
3: 0x3ab208003f09 <String[3]: #add>
Handler Table (size = 0)
Source Position Table (size = 0)
[generated bytecode for function: add (0x3ab20829344d <SharedFunctionInfo add>)]
Bytecode length: 6
// 承受 3 个参数,1 个隐式的 this,以及显式的 x 和 y
Parameter count 3
Register count 0
// 不须要局部变量,因而帧大小为 0
Frame size 0
OSR nesting level: 0
Bytecode Age: 0
0x3ab2082935f6 @ 0 : 0b 04 Ldar a1
0x3ab2082935f8 @ 2 : 39 03 00 Add a0, [0]
0x3ab2082935fb @ 5 : a9 Return
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
add 函数次要蕴含以下 3 个字节码序列:
// Load Accumulator Register
// 加载寄存器 a1 的值到累加器中
Ldar a1
// 读取寄存器 a0 的值并累加到累加器中,相加之后的后果会持续放在累加器中
// [0] 指向 Feedback Vector Slot,Ignition 会收集值的剖析信息,为后续的 TurboFan 优化做筹备
Add a0, [0]
// 转交控制权给调用者,并返回累加器中的值
Return
这里 Ignition 的解释执行这些字节码采纳的是一地址指令构造的寄存器架构。
对于更多字节码的信息可查看 Understanding V8’s Bytecode。
4 优化和去优化
JavaScript 是弱类型语言,不会像强类型语言那样须要限定函数调用的形参数据类型,而是能够非常灵活的传入各种类型的参数进行解决,如下所示:
function add(x, y) {
// + 操作符是 JavaScript 中非常复杂的一个操作
return x + y
}
add(1, 2);
add('1', 2);
add(, 2);
add(undefined, 2);
add([], 2);
add({}, 2);
add([], {});
为了能够进行 + 操作符运算,在底层执行的时候往往须要调用很多 API,比方 ToPrimitive(判断是否是对象)、ToString、ToNumber 等,将传入的参数进行合乎 + 操作符的数据转换解决。
在这里 V8 会对 JavaScript 像强类型语言那样对形参 x 和 y 进行揣测,这样就能够在运行的过程中排除一些副作用分支代码,同时这里也会预测代码不会抛出异样,因而能够对代码进行优化,从而达到最高的运行性能。在 Ignition 中通过字节码来收集反馈信息(Feedback Vector),如下所示:
为了查看 add 函数的运行时反馈信息,咱们能够通过 V8 提供的 Native API 来打印 add 函数的运行时信息,具体如下所示:
function add(x, y) {return x + y}
// 留神这里默认采纳了 ClosureFeedbackCellArray,为了查看成果,强制开启 FeedbackVector
// 更多信息查看:A lighter V8:https://v8.dev/blog/v8-lite
%EnsureFeedbackVectorForFunction(add);
add(1, 2);
// 打印 add 具体的运行时信息
%DebugPrint(add);
通过 –allow-natives-syntax 参数能够在 JavaScript 中调用 %DebugPrint 底层 Native API(更多 API 能够查看 V8 的 runtime.h 头文件):
这里的 SharedFunctionInfo(SFI)中保留了一个 InterpreterEntryTrampoline 指针信息,每个函数都会有一个指向 Ignition 解释器的 trampoline 指针,每当 V8 须要进去去优化时,就会应用此指针使代码回退到解释器相应的函数执行地位。
为了使得 add 函数能够像 HotSpot 代码一样被优化,在这里强制做一次函数优化:
通过 –trace-opt 参数能够跟踪 add 函数的编译优化信息:
须要留神的是 V8 会主动监测代码的构造变动,从而执行去优化。例如下述代码:
function add(x, y) {return x + y}
%EnsureFeedbackVectorForFunction(add);
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);
// 扭转 add 函数的传入参数类型,之前都是 number 类型,这里传入 string 类型
add(1, '2');
%DebugPrint(add);
咱们能够通过 –trace-deopt 参数跟踪 add 函数的去优化信息:
须要留神的是代码在执行去优化的过程中会产生性能损耗,因而在日常的开发中,倡议应用 TypeScript 对代码进行类型申明,这样能够肯定水平晋升代码的性能。
五 总结
本文对于 V8 的钻研还处在一个理性的认知阶段,并没有深刻到 V8 底层的源码。通过本文能够对 V8 的编译原理有一个理性的认知,同时也倡议大家能够应用 TypeScript,它的确能在肯定水平上对 JavaScript 代码的编写产生更好的指导作用。
原文链接
本文为阿里云原创内容,未经容许不得转载。