V8 编译浅谈
簡(jiǎn)介:本文是一個(gè) V8 編譯原理知識(shí)的介紹文章,旨在讓大家感性的了解 JavaScript 在 V8 中的解析過程。
作者 | 子弈
來源 | 阿里技術(shù)公眾號(hào)
一 簡(jiǎn)介
本文是一個(gè) V8 編譯原理知識(shí)的介紹文章,旨在讓大家感性的了解 JavaScript 在 V8 中的解析過程。本文主要的撰寫流程如下:
- 解釋器和編譯器:計(jì)算機(jī)編譯原理的基礎(chǔ)知識(shí)介紹
- V8 的編譯原理:基于計(jì)算機(jī)編譯原理的知識(shí),了解 V8 對(duì)于 JavaScript 的解析流程
- V8 的運(yùn)行時(shí)表現(xiàn):結(jié)合 V8 的編譯原理,實(shí)踐 V8 在解析流程中的具體運(yùn)行表現(xiàn)
二 解釋器和編譯器
大家可能一直疑惑的問題:JavaScript 是一門解釋型語言嗎?要了解這個(gè)問題,首先需要初步了解什么是解釋器和編譯器以及它們的特點(diǎn)是什么。
1 解釋器
解釋器的作用是將某種語言編寫的源程序作為輸入,將該源程序執(zhí)行的結(jié)果作為輸出,例如 Perl、Scheme、APL 等都是使用解釋器進(jìn)行轉(zhuǎn)換執(zhí)行:
2 編譯器
編譯器的設(shè)計(jì)是一個(gè)非常龐大和復(fù)雜的軟件系統(tǒng)設(shè)計(jì),在真正設(shè)計(jì)的時(shí)候需要解決兩個(gè)相對(duì)重要的問題:
- 如何分析不同高級(jí)程序語言設(shè)計(jì)的源程序
- 如何將源程序的功能等價(jià)映射到不同指令系統(tǒng)的目標(biāo)機(jī)器
中間表示(IR)
中間表示(Intermediate Representation,IR)是程序結(jié)構(gòu)的一種表現(xiàn)方式,它會(huì)比抽象語法樹(Abstract Syntax Tree,AST)更加接近匯編語言或者指令集,同時(shí)也會(huì)保留源程序中的一些高級(jí)信息,具體作用包括:
- 易于編譯器的錯(cuò)誤調(diào)試,容易識(shí)別是 IR 之前的前端還是之后的后端出的問題
- 可以使得編譯器的職責(zé)更加分離,源程序的編譯更多關(guān)注如何轉(zhuǎn)換成 IR,而不是去適配不同的指令集
- IR 更加接近指令集,從而相對(duì)于源碼可以更加節(jié)省內(nèi)存空間
優(yōu)化編譯器
IR 本身可以做到多趟迭代從而優(yōu)化源程序,在每一趟迭代的過程中可以研究代碼并記錄優(yōu)化的細(xì)節(jié),方便后續(xù)的迭代查找并利用這些優(yōu)化信息,最終可以高效輸出更優(yōu)的目標(biāo)程序:
優(yōu)化器可以對(duì) IR 進(jìn)行一趟或者多趟處理,從而生成更快執(zhí)行速度或者更小體積的目標(biāo)程序(例如找到循環(huán)中不變的計(jì)算并對(duì)其進(jìn)行優(yōu)化從而減少運(yùn)算次數(shù)),也可能用于產(chǎn)生更少異常或者更低功耗的目標(biāo)程序。除此之外,前端和后端內(nèi)部還可以細(xì)分為多個(gè)處理步驟,具體如下圖所示:
3 兩者的特性比較
解釋器和編譯器的具體特性比較如下所示:
需要注意早期的 Web 前端要求頁面的啟動(dòng)速度快,因此采用解釋執(zhí)行的方式,但是頁面在運(yùn)行的過程中性能相對(duì)較低。為了解決這個(gè)問題,需要在運(yùn)行時(shí)對(duì) JavaScript 代碼進(jìn)行優(yōu)化,因此在 JavaScript 的解析引擎中引入了 JIT 技術(shù)。
4 JIT 編譯技術(shù)
JIT (Just In Time)編譯器是一種動(dòng)態(tài)編譯技術(shù),相對(duì)于傳統(tǒng)編譯器而言,最大的區(qū)別在于編譯時(shí)和運(yùn)行時(shí)不分離,是一種在運(yùn)行的過程中對(duì)代碼進(jìn)行動(dòng)態(tài)編譯的技術(shù)。
5 混合動(dòng)態(tài)編譯技術(shù)
為了解決 JavaScript 在運(yùn)行時(shí)性能較慢的問題,可以通過引入 JIT 技術(shù),并采用混合動(dòng)態(tài)編譯的方式來提升 JavaScript 的運(yùn)行性能,具體思路如下所示:
采用上述編譯框架后,可以使得 JavaScript 語言:
- 啟動(dòng)速度快:在 JavaScript 啟動(dòng)的時(shí)候采用解釋執(zhí)行的方式運(yùn)行,利用了解釋器啟動(dòng)速度快的特性
- 運(yùn)行性能高:在 JavaScript 運(yùn)行的過程中可以對(duì)代碼進(jìn)行監(jiān)控,從而使用 JIT 技術(shù)對(duì)代碼進(jìn)行編譯優(yōu)化
三 V8 的編譯原理
V8 是一個(gè)開源的 JavaScript 虛擬機(jī),目前主要用在 Chrome 瀏覽器(包括開源的 Chromium)以及 Node.js 中,核心功能是用于解析和執(zhí)行 JavaScript 語言。為了解決早期 JavaScript 運(yùn)行性能差的問題,V8 經(jīng)歷了多個(gè)歷史的編譯框架衍變之后(感興趣的同學(xué)可以了解一下早期的 V8 編譯框架設(shè)計(jì)),引入混合動(dòng)態(tài)編譯的技術(shù)來解決問題,具體詳細(xì)的編譯框架如下所示:
1 Ignition 解釋器
Ignition 的主要作用是將 AST 轉(zhuǎn)換成 Bytecode(字節(jié)碼,中間表示)。在運(yùn)行的過程中,還會(huì)使用類型反饋(TypeFeedback)技術(shù)并計(jì)算熱點(diǎn)代碼(HotSpot,重復(fù)被運(yùn)行的代碼,可以是方法也可以是循環(huán)體),最終交給 TurboFan 進(jìn)行動(dòng)態(tài)運(yùn)行時(shí)的編譯優(yōu)化。Ignition 的解釋執(zhí)行流程如下所示:
在字節(jié)碼解釋執(zhí)行的過程中,會(huì)將需要進(jìn)行性能優(yōu)化的運(yùn)行時(shí)信息指向?qū)?yīng)的 Feedback Vector(反饋向量,之前也被稱為 Type Feedback Vector),Feeback Vector 中會(huì)包含根據(jù)內(nèi)聯(lián)緩存(Inline Cache,IC)來存儲(chǔ)的多種類型的插槽(Feedback Vector Slot)信息,例如 BinaryOp 插槽(二進(jìn)制操作結(jié)果的數(shù)據(jù)類型)、Invocation Count(函數(shù)的調(diào)用次數(shù))以及 Optimized Code 信息等。
這里不會(huì)過多講解每個(gè)執(zhí)行流程的細(xì)節(jié)問題。2 TurboFan 優(yōu)化編譯器
TurboFan 利用了 JIT 編譯技術(shù),主要作用是對(duì) JavaScript 代碼進(jìn)行運(yùn)行時(shí)編譯優(yōu)化,具體的流程如下所示:
圖片出處 An Introduction to Speculative Optimization in V8。需要注意 Profiling Feedback 部分,這里主要提供 Ignition 解釋執(zhí)行過程中生成的運(yùn)行時(shí)反饋向量信息 Feedback Vector ,Turbofan 會(huì)結(jié)合字節(jié)碼以及反饋向量信息生成圖示(數(shù)據(jù)結(jié)構(gòu)中的圖結(jié)構(gòu)),并將圖傳遞給前端部分,之后會(huì)根據(jù)反饋向量信息對(duì)代碼進(jìn)行優(yōu)化和去優(yōu)化。
這里的去優(yōu)化是指讓代碼回退到 Ignition 進(jìn)行解釋執(zhí)行,去優(yōu)化本質(zhì)是因?yàn)闄C(jī)器碼已經(jīng)不能滿足運(yùn)行訴求,例如一個(gè)變量從 string 類型轉(zhuǎn)變成 number 類型,機(jī)器碼編譯的是 string 類型,此時(shí)已經(jīng)無法再滿足運(yùn)行訴求,因此 V8 會(huì)執(zhí)行去優(yōu)化動(dòng)作,將代碼回退到 Ignition 進(jìn)行解釋執(zhí)行。四 V8 的運(yùn)行時(shí)表現(xiàn)
在了解 V8 的編譯原理之后,接下來需要使用 V8 的調(diào)試工具來具體查看 JavaScript 的編譯和運(yùn)行信息,從而加深我們對(duì) V8 的編譯過程認(rèn)知。
1 D8 調(diào)試工具
如果想了解 JavaScript 在 V8 中的編譯時(shí)和運(yùn)行時(shí)信息,可以使用調(diào)試工具 D8。D8 是 V8 引擎的命令行 Shell,可以查看 AST 生成、中間代碼 ByteCode、優(yōu)化代碼、反優(yōu)化代碼、優(yōu)化編譯器的統(tǒng)計(jì)數(shù)據(jù)、代碼的 GC 等信息。D8 的安裝方式有很多,如下所示:
- 方法一:根據(jù) V8 官方文檔 Using d8 以及 Building V8 with GN 進(jìn)行工具鏈的下載和編譯
- 方法二:使用別人已經(jīng)編譯好的 D8 工具,可能版本會(huì)有滯后性,例如 Mac 版
- 方法三:使用 JavaScript 引擎版本管理工具,例如 jsvu,可以下載到最新編譯好的 JavaScript 引擎
本文使用方法三安裝 v8-debug 工具,安裝完成后執(zhí)行 v8-debug --help 可以查看有哪些命令:
# 執(zhí)行 help 命令查看支持的參數(shù) v8-debug --helpSynopsis: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 snapshotSSE3=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:# 打印生成的字節(jié)碼--print-bytecode (print bytecode generated by ignition interpreter)type: bool default: --noprint-bytecode# 跟蹤被優(yōu)化的信息--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# 跟蹤去優(yōu)化的信息--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# 查看優(yōu)化后的代碼--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-syntax2 生成 AST
我們編寫一個(gè) index.js 文件,在文件中寫入 JavaScript 代碼,執(zhí)行一個(gè)簡(jiǎn)單的 add 函數(shù):
function add(x, y) {return x + y }console.log(add(1, 2));使用 --print-ast 參數(shù)可以打印 add 函數(shù)的 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 節(jié)點(diǎn)在真正的分析階段會(huì)連接到對(duì)應(yīng)地址的 VAR 節(jié)點(diǎn)。
3 生成字節(jié)碼
AST 會(huì)經(jīng)過 Ignition 解釋器的 BytecodeGenerator 函數(shù)生成字節(jié)碼(中間表示),我們可以通過 --print-bytecode 參數(shù)來打印字節(jié)碼信息:
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: 00x3ab2082934be @ 0 : 13 00 LdaConstant [0]0x3ab2082934c0 @ 2 : c3 Star1 0x3ab2082934c1 @ 3 : 19 fe f8 Mov <closure>, r20x3ab2082934c4 @ 6 : 65 52 01 f9 02 CallRuntime [DeclareGlobals], r1-r20x3ab2082934c9 @ 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: 40: 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 個(gè)參數(shù), 1 個(gè)隱式的 this,以及顯式的 x 和 y Parameter count 3 Register count 0 // 不需要局部變量,因此幀大小為 0 Frame size 0 OSR nesting level: 0 Bytecode Age: 00x3ab2082935f6 @ 0 : 0b 04 Ldar a10x3ab2082935f8 @ 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 函數(shù)主要包含以下 3 個(gè)字節(jié)碼序列:
// Load Accumulator Register // 加載寄存器 a1 的值到累加器中 Ldar a1 // 讀取寄存器 a0 的值并累加到累加器中,相加之后的結(jié)果會(huì)繼續(xù)放在累加器中 // [0] 指向 Feedback Vector Slot,Ignition 會(huì)收集值的分析信息,為后續(xù)的 TurboFan 優(yōu)化做準(zhǔn)備 Add a0, [0] // 轉(zhuǎn)交控制權(quán)給調(diào)用者,并返回累加器中的值 Return這里 Ignition 的解釋執(zhí)行這些字節(jié)碼采用的是一地址指令結(jié)構(gòu)的寄存器架構(gòu)。
關(guān)于更多字節(jié)碼的信息可查看 Understanding V8’s Bytecode。4 優(yōu)化和去優(yōu)化
JavaScript 是弱類型語言,不會(huì)像強(qiáng)類型語言那樣需要限定函數(shù)調(diào)用的形參數(shù)據(jù)類型,而是可以非常靈活的傳入各種類型的參數(shù)進(jìn)行處理,如下所示:
function add(x, y) { // + 操作符是 JavaScript 中非常復(fù)雜的一個(gè)操作return x + y }add(1, 2); add('1', 2); add(, 2); add(undefined, 2); add([], 2); add({}, 2); add([], {});為了可以進(jìn)行 + 操作符運(yùn)算,在底層執(zhí)行的時(shí)候往往需要調(diào)用很多 API,比如 ToPrimitive(判斷是否是對(duì)象)、ToString、ToNumber 等,將傳入的參數(shù)進(jìn)行符合 + 操作符的數(shù)據(jù)轉(zhuǎn)換處理。
在這里 V8 會(huì)對(duì) JavaScript 像強(qiáng)類型語言那樣對(duì)形參 x 和 y 進(jìn)行推測(cè),這樣就可以在運(yùn)行的過程中排除一些副作用分支代碼,同時(shí)這里也會(huì)預(yù)測(cè)代碼不會(huì)拋出異常,因此可以對(duì)代碼進(jìn)行優(yōu)化,從而達(dá)到最高的運(yùn)行性能。在 Ignition 中通過字節(jié)碼來收集反饋信息(Feedback Vector),如下所示:
為了查看 add 函數(shù)的運(yùn)行時(shí)反饋信息,我們可以通過 V8 提供的 Native API 來打印 add 函數(shù)的運(yùn)行時(shí)信息,具體如下所示:
function add(x, y) {return x + y }// 注意這里默認(rèn)采用了 ClosureFeedbackCellArray,為了查看效果,強(qiáng)制開啟 FeedbackVector // 更多信息查看: A lighter V8:https://v8.dev/blog/v8-lite %EnsureFeedbackVectorForFunction(add); add(1, 2); // 打印 add 詳細(xì)的運(yùn)行時(shí)信息 %DebugPrint(add);通過 --allow-natives-syntax 參數(shù)可以在 JavaScript 中調(diào)用 %DebugPrint 底層 Native API(更多 API 可以查看 V8 的 runtime.h 頭文件):
?
這里的 SharedFunctionInfo(SFI)中保留了一個(gè) InterpreterEntryTrampoline 指針信息,每個(gè)函數(shù)都會(huì)有一個(gè)指向 Ignition 解釋器的 trampoline 指針,每當(dāng) V8 需要進(jìn)去去優(yōu)化時(shí),就會(huì)使用此指針使代碼回退到解釋器相應(yīng)的函數(shù)執(zhí)行位置。
為了使得 add 函數(shù)可以像 HotSpot 代碼一樣被優(yōu)化,在這里強(qiáng)制做一次函數(shù)優(yōu)化:
通過 --trace-opt 參數(shù)可以跟蹤 add 函數(shù)的編譯優(yōu)化信息:
?
需要注意的是 V8 會(huì)自動(dòng)監(jiān)測(cè)代碼的結(jié)構(gòu)變化,從而執(zhí)行去優(yōu)化。例如下述代碼:
function add(x, y) {return x + y }%EnsureFeedbackVectorForFunction(add);add(1, 2); %OptimizeFunctionOnNextCall(add); add(1, 2); // 改變 add 函數(shù)的傳入?yún)?shù)類型,之前都是 number 類型,這里傳入 string 類型 add(1, '2'); %DebugPrint(add);我們可以通過 --trace-deopt 參數(shù)跟蹤 add 函數(shù)的去優(yōu)化信息:
?
需要注意的是代碼在執(zhí)行去優(yōu)化的過程中會(huì)產(chǎn)生性能損耗,因此在日常的開發(fā)中,建議使用 TypeScript 對(duì)代碼進(jìn)行類型聲明,這樣可以一定程度提升代碼的性能。
五 總結(jié)
本文對(duì)于 V8 的研究還處在一個(gè)感性的認(rèn)知階段,并沒有深入到 V8 底層的源碼。通過本文可以對(duì) V8 的編譯原理有一個(gè)感性的認(rèn)知,同時(shí)也建議大家可以使用 TypeScript,它確實(shí)能在一定程度上對(duì) JavaScript 代碼的編寫產(chǎn)生更好的指導(dǎo)作用。
原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。?
總結(jié)
- 上一篇: 代码智能技术如何应用到日常开发?
- 下一篇: “预习-上课-复习”:达摩院类人学习新范