天天看點

V8 編譯淺談

簡介

本文是一個 V8 編譯原理知識的介紹文章,旨在讓大家感性的了解 JavaScript 在 V8 中的解析過程。本文主要的撰寫流程如下:

  • 解釋器和編譯器:計算機編譯原理的基礎知識介紹
  • V8 的編譯原理:基于計算機編譯原理的知識,了解 V8 對于 JavaScript 的解析流程
  • V8 的運作時表現:結合 V8 的編譯原理,實踐 V8 在解析流程中的具體運作表現
溫馨提示:本文僅代表個人觀點,文中若有錯誤歡迎指正。

解釋器和編譯器

大家可能一直疑惑的問題:JavaScript 是一門解釋型語言嗎?要了解這個問題,首先需要初步了解什麼是解釋器和編譯器以及它們的特點是什麼。

解釋器

解釋器的作用是将某種語言編寫的源程式作為輸入,将該源程式執行的結果作為輸出,例如 Perl、Scheme、APL 等都是使用解釋器進行轉換執行:

V8 編譯淺談

編譯器

編譯器的設計是一個非常龐大和複雜的軟體系統設計,在真正設計的時候需要解決兩個相對重要的問題:

  • 如何分析不同進階程式語言設計的源程式
  • 如何将源程式的功能等價映射到不同指令系統的目标機器
V8 編譯淺談

中間表示(IR)

中間表示(Intermediate Representation,IR)是程式結構的一種表現方式,它會比抽象文法樹(Abstract Syntax Tree,AST)更加接近彙編語言或者指令集,同時也會保留源程式中的一些進階資訊,具體作用包括:

  • 易于編譯器的錯誤調試,容易識别是 IR 之前的前端還是之後的後端出的問題
  • 可以使得編譯器的職責更加分離,源程式的編譯更多關注如何轉換成 IR,而不是去适配不同的指令集
  • IR 更加接近指令集,進而相對于源碼可以更加節省記憶體空間
V8 編譯淺談

優化編譯器

IR 本身可以做到多趟疊代進而優化源程式,在每一趟疊代的過程中可以研究代碼并記錄優化的細節,友善後續的疊代查找并利用這些優化資訊,最終可以高效輸出更優的目标程式:

V8 編譯淺談

優化器可以對 IR 進行一趟或者多趟處理,進而生成更快執行速度或者更小體積的目标程式(例如找到循環中不變的計算并對其進行優化進而減少運算次數),也可能用于産生更少異常或者更低功耗的目标程式。除此之外,前端和後端内部還可以細分為多個處理步驟,具體如下圖所示:

V8 編譯淺談

兩者的特性比較

解釋器和編譯器的具體特性比較如下所示:

類型
工作機制 編譯和執行同時運作 編譯和執行分離
啟動速度 相對較快 相對較慢
運作性能 相對較低 相對較高
錯誤檢測 運作時檢測 編譯時檢測

需要注意早期的 Web 前端要求頁面的啟動速度快,是以采用解釋執行的方式,但是頁面在運作的過程中性能相對較低。為了解決這個問題,需要在運作時對 JavaScript 代碼進行優化,是以在 JavaScript 的解析引擎中引入了 JIT 技術。

JIT 編譯技術

JIT (Just In Time)編譯器是一種動态編譯技術,相對于傳統編譯器而言,最大的差別在于編譯時和運作時不分離,是一種在運作的過程中對代碼進行動态編譯的技術。

JIT 編譯器
根據優化情況而定,一般會比解釋器性能更好

混合動态編譯技術

為了解決 JavaScript 在運作時性能較慢的問題,可以通過引入 JIT 技術,并采用混合動态編譯的方式來提升 JavaScript 的運作性能,具體思路如下所示:

V8 編譯淺談

采用上述編譯架構後,可以使得 JavaScript 語言:

  • 啟動速度快:在 JavaScript 啟動的時候采用解釋執行的方式運作,利用了解釋器啟動速度快的特性
  • 運作性能高:在 JavaScript 運作的過程中可以對代碼進行監控,進而使用 JIT 技術對代碼進行編譯優化

V8 的編譯原理

V8

是一個開源的 JavaScript 虛拟機,目前主要用在 Chrome 浏覽器(包括開源的

Chromium

)以及 Node.js 中,核心功能是用于解析和執行 JavaScript 語言。為了解決早期 JavaScript 運作性能差的問題,V8 經曆了多個曆史的編譯架構衍變之後(感興趣的同學可以了解一下早期的 V8 編譯架構設計),引入混合動态編譯的技術來解決問題,具體詳細的編譯架構如下所示:

V8 編譯淺談

Ignition 解釋器

Ignition 的主要作用是将 AST 轉換成

Bytecode

(位元組碼,中間表示)。在運作的過程中,還會使用類型回報(TypeFeedback)技術并計算熱點代碼(HotSpot,重複被運作的代碼,可以是方法也可以是循環體),最終交給 TurboFan 進行動态運作時的編譯優化。Ignition 的解釋執行流程如下所示:

V8 編譯淺談

在位元組碼解釋執行的過程中,會将需要進行性能優化的運作時資訊指向對應的 Feedback Vector(

回報向量

,之前也被稱為 Type Feedback Vector),Feeback Vector 中會包含根據内聯緩存(Inline Cache,IC)來存儲的多種類型的插槽(Feedback Vector Slot)資訊,例如 BinaryOp 插槽(二進制操作結果的資料類型)、Invocation Count(函數的調用次數)以及 Optimized Code 資訊等。

溫馨提示:這裡不會過多講解每個執行流程的細節問題,會在後續的系列文章中進行講解。上述透出的回報向量資訊(Feedback Vector)會在接下來的 V8 運作時示範中進行資訊列印。

TurboFan 優化編譯器

TurboFan 利用了 JIT 編譯技術,主要作用是對 JavaScript 代碼進行運作時編譯優化,具體的流程如下所示:

V8 編譯淺談
溫馨提示:圖檔出處 An Introduction to Speculative Optimization in V8

需要注意 Profiling Feedback 部分,這裡主要提供 Ignition 解釋執行過程中生成的運作時回報向量資訊 Feedback Vector ,Turbofan 會結合位元組碼以及回報向量資訊生成圖示(資料結構中的圖結構),并将圖傳遞給前端部分,之後會根據回報向量資訊對代碼進行優化和去優化。

溫馨提示:這裡的去優化是指讓代碼回退到 Ignition 進行解釋執行,去優化本質是因為機器碼已經不能滿足運作訴求,例如一個變量從 string 類型轉變成 number 類型,機器碼編譯的是 string 類型,此時已經無法再滿足運作訴求,是以 V8 會執行去優化動作,将代碼回退到 Ignition 進行解釋執行。

V8 的運作時表現

在了解 V8 的編譯原理之後,接下來需要使用 V8 的調試工具來具體檢視 JavaScript 的編譯和運作資訊,進而加深我們對 V8 的編譯過程認知。

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
           

生成 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 樹:

V8 編譯淺談

VAR PROXY 節點在真正的分析階段會連接配接到對應位址的 VAR 節點。

生成位元組碼

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 。這裡的寄存器架構在後續的系列文章中進行詳細講解。

優化和去優化

JavaScript 是弱類型語言,不會像強類型語言那樣需要限定函數調用的形參資料類型,而是可以非常靈活的傳入各種類型的參數進行處理,如下所示:

function add(x, y) { 
    // + 操作符是 JavaScript 中非常複雜的一個操作
    return x + y
}

add(1, 2);
add('1', 2);
add(null, 2);
add(undefined, 2);
add([], 2);
add({}, 2);
add([], {});           

為了可以進行 + 操作符運算,在底層執行的時候往往需要調用很多 API,比如 ToPrimitive(判斷是否是對象)、ToString、ToNumber 等,将傳入的參數進行符合 + 操作符的資料轉換處理。

在這裡 V8 會對 JavaScript 像強類型語言那樣對形參 x 和 y 進行推測,這樣就可以在運作的過程中排除一些副作用分支代碼,同時這裡也會預測代碼不會抛出異常,是以可以對代碼進行優化,進而達到最高的運作性能。在 Ignition 中通過位元組碼來收集回報資訊(Feedback Vector),如下所示:

V8 編譯淺談

為了檢視 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

頭檔案):

v8-debug --allow-natives-syntax  ./index.js

DebugPrint: 0x1d22082935b9: [Function] in OldSpace
 - map: 0x1d22082c2281 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1d2208283b79 <JSFunction (sfi = 0x1d220820abbd)>
 - elements: 0x1d220800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: 
 - initial_map: 
 - shared_info: 0x1d2208293491 <SharedFunctionInfo add>
 - name: 0x1d2208003f09 <String[3]: #add>
 // 包含 Ignition 解釋器的 trampoline 指針
 - builtin: InterpreterEntryTrampoline
 - formal_parameter_count: 2
 - kind: NormalFunction
 - context: 0x1d2208283649 <NativeContext[263]>
 - code: 0x1d2200005181 <Code BUILTIN InterpreterEntryTrampoline>
 - interpreted
 - bytecode: 0x1d2208293649 <BytecodeArray[6]>
 - source code: (x, y) {
    return x + y
}
 - properties: 0x1d220800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x1d2208004bb5: [String] in ReadOnlySpace: #length: 0x1d2208204431 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x1d2208004dfd: [String] in ReadOnlySpace: #name: 0x1d22082043ed <AccessorInfo> (const accessor descriptor), location: descriptor
    0x1d2208003fad: [String] in ReadOnlySpace: #arguments: 0x1d2208204365 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x1d22080041f1: [String] in ReadOnlySpace: #caller: 0x1d22082043a9 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x1d22080050b1: [String] in ReadOnlySpace: #prototype: 0x1d2208204475 <AccessorInfo> (const accessor descriptor), location: descriptor
 }

 // 以下是詳細的回報資訊 
 - feedback vector: 0x1d2208293691: [FeedbackVector] in OldSpace
 - map: 0x1d2208002711 <Map>
 - length: 1
 - shared function info: 0x1d2208293491 <SharedFunctionInfo add>
 - no optimized code
 - optimization marker: OptimizationMarker::kNone
 - optimization tier: OptimizationTier::kNone
 - invocation count: 0
 - profiler ticks: 0
 - closure feedback cell array: 0x1d22080032b5: [ClosureFeedbackCellArray] in ReadOnlySpace
 - map: 0x1d2208002955 <Map>
 - length: 0

 - slot #0 BinaryOp BinaryOp:None {
     [0]: 0
  }
0x1d22082c2281: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - callable
 - constructor
 - has_prototype_slot
 - back pointer: 0x1d22080023b5 <undefined>
 - prototype_validity cell: 0x1d22082044fd <Cell value= 1>
 - instance descriptors (own) #5: 0x1d2208283c29 <DescriptorArray[5]>
 - prototype: 0x1d2208283b79 <JSFunction (sfi = 0x1d220820abbd)>
 - constructor: 0x1d2208283bf5 <JSFunction Function (sfi = 0x1d220820acb9)>
 - dependent code: 0x1d22080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0           
溫馨提示:這裡的 SharedFunctionInfo(SFI)中保留了一個 InterpreterEntryTrampoline 指針資訊,每個函數都會有一個指向 Ignition 解釋器的 trampoline 指針,每當 V8 需要進去去優化時,就會使用此指針使代碼回退到解釋器相應的函數執行位置。

為了使得 add 函數可以像 HotSpot 代碼一樣被優化,在這裡強制做一次函數優化:

function add(x, y) {
    return x + y
}

add(1, 2);
// 強制開啟函數優化
%OptimizeFunctionOnNextCall(add);
%EnsureFeedbackVectorForFunction(add);
add(1, 2);
// 列印 add 詳細的運作時資訊    
%DebugPrint(add);           

通過 --trace-opt 參數可以跟蹤 add 函數的編譯優化資訊:

v8-debug --allow-natives-syntax --trace-opt  ./index.js

[manually marking 0x3872082935bd <JSFunction add (sfi = 0x3872082934b9)> for non-concurrent optimization]
// 這裡使用 TurboFan 優化編譯器對 add 函數進行編譯優化
[compiling method 0x3872082935bd <JSFunction add (sfi = 0x3872082934b9)> (target TURBOFAN) using TurboFan]
[optimizing 0x3872082935bd <JSFunction add (sfi = 0x3872082934b9)> (target TURBOFAN) - took 0.097, 2.003, 0.273 ms]
DebugPrint: 0x3872082935bd: [Function] in OldSpace
 - map: 0x3872082c2281 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x387208283b79 <JSFunction (sfi = 0x38720820abbd)>
 - elements: 0x38720800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: 
 - initial_map: 
 - shared_info: 0x3872082934b9 <SharedFunctionInfo add>
 - name: 0x387208003f09 <String[3]: #add>
 - formal_parameter_count: 2
 - kind: NormalFunction
 - context: 0x387208283649 <NativeContext[263]>
 - code: 0x387200044001 <Code TURBOFAN>
 - source code: (x, y) {
    return x + y
}
 - properties: 0x38720800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x387208004bb5: [String] in ReadOnlySpace: #length: 0x387208204431 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x387208004dfd: [String] in ReadOnlySpace: #name: 0x3872082043ed <AccessorInfo> (const accessor descriptor), location: descriptor
    0x387208003fad: [String] in ReadOnlySpace: #arguments: 0x387208204365 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x3872080041f1: [String] in ReadOnlySpace: #caller: 0x3872082043a9 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x3872080050b1: [String] in ReadOnlySpace: #prototype: 0x387208204475 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - feedback vector: 0x387208293685: [FeedbackVector] in OldSpace
 - map: 0x387208002711 <Map>
 - length: 1
 - shared function info: 0x3872082934b9 <SharedFunctionInfo add>
 - no optimized code
 - optimization marker: OptimizationMarker::kNone
 - optimization tier: OptimizationTier::kNone
 // 調用次數增加了 1 次
 - invocation count: 1
 - profiler ticks: 0
 - closure feedback cell array: 0x3872080032b5: [ClosureFeedbackCellArray] in ReadOnlySpace
 - map: 0x387208002955 <Map>
 - length: 0

 - slot #0 BinaryOp BinaryOp:SignedSmall {
     [0]: 1
  }
0x3872082c2281: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - callable
 - constructor
 - has_prototype_slot
 - back pointer: 0x3872080023b5 <undefined>
 - prototype_validity cell: 0x3872082044fd <Cell value= 1>
 - instance descriptors (own) #5: 0x387208283c29 <DescriptorArray[5]>
 - prototype: 0x387208283b79 <JSFunction (sfi = 0x38720820abbd)>
 - constructor: 0x387208283bf5 <JSFunction Function (sfi = 0x38720820acb9)>
 - dependent code: 0x3872080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0           

需要注意的是 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 函數的去優化資訊:

ziyi@B-D0UTG8WN-2029 .jsvu % v8-debug --allow-natives-syntax --trace-deopt  ./index.js
// 執行去優化,reason: not a Smi(Smi 在後續的系列文章中進行講解,這裡說明傳入的不是一個小整數類型)
[bailout (kind: deopt-eager, reason: not a Smi: begin. deoptimizing 0x08f70829363d <JSFunction add (sfi = 0x8f7082934c9)>, opt id 0, node id 58, bytecode offset 2, deopt exit 1, FP to SP delta 32, caller SP 0x7ffee9ce7d70, pc 0x08f700044162]
DebugPrint: 0x8f70829363d: [Function] in OldSpace
 - map: 0x08f7082c2281 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x08f708283b79 <JSFunction (sfi = 0x8f70820abbd)>
 - elements: 0x08f70800222d <FixedArray[0]> [HOLEY_ELEMENTS]
 - function prototype: 
 - initial_map: 
 - shared_info: 0x08f7082934c9 <SharedFunctionInfo add>
 - name: 0x08f708003f09 <String[3]: #add>
 - formal_parameter_count: 2
 - kind: NormalFunction
 - context: 0x08f708283649 <NativeContext[263]>
 - code: 0x08f700044001 <Code TURBOFAN>
 - interpreted
 - bytecode: 0x08f7082936cd <BytecodeArray[6]>
 - source code: (x, y) {
    return x + y
}
 - properties: 0x08f70800222d <FixedArray[0]>
 - All own properties (excluding elements): {
    0x8f708004bb5: [String] in ReadOnlySpace: #length: 0x08f708204431 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x8f708004dfd: [String] in ReadOnlySpace: #name: 0x08f7082043ed <AccessorInfo> (const accessor descriptor), location: descriptor
    0x8f708003fad: [String] in ReadOnlySpace: #arguments: 0x08f708204365 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x8f7080041f1: [String] in ReadOnlySpace: #caller: 0x08f7082043a9 <AccessorInfo> (const accessor descriptor), location: descriptor
    0x8f7080050b1: [String] in ReadOnlySpace: #prototype: 0x08f708204475 <AccessorInfo> (const accessor descriptor), location: descriptor
 }
 - feedback vector: 0x8f708293715: [FeedbackVector] in OldSpace
 - map: 0x08f708002711 <Map>
 - length: 1
 - shared function info: 0x08f7082934c9 <SharedFunctionInfo add>
 - no optimized code
 - optimization marker: OptimizationMarker::kNone
 - optimization tier: OptimizationTier::kNone
 - invocation count: 1
 - profiler ticks: 0
 - closure feedback cell array: 0x8f7080032b5: [ClosureFeedbackCellArray] in ReadOnlySpace
 - map: 0x08f708002955 <Map>
 - length: 0

 - slot #0 BinaryOp BinaryOp:Any {
     [0]: 127
  }
0x8f7082c2281: [Map]
 - type: JS_FUNCTION_TYPE
 - instance size: 32
 - inobject properties: 0
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - callable
 - constructor
 - has_prototype_slot
 - back pointer: 0x08f7080023b5 <undefined>
 - prototype_validity cell: 0x08f7082044fd <Cell value= 1>
 - instance descriptors (own) #5: 0x08f708283c29 <DescriptorArray[5]>
 - prototype: 0x08f708283b79 <JSFunction (sfi = 0x8f70820abbd)>
 - constructor: 0x08f708283bf5 <JSFunction Function (sfi = 0x8f70820acb9)>
 - dependent code: 0x08f7080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0           

需要注意的是代碼在執行去優化的過程中會産生性能損耗,是以在日常的開發中,建議使用 TypeScript 對代碼進行類型聲明,這樣可以一定程度提升代碼的性能。

總結

本文對于 V8 的研究還處在一個感性的認知階段,并沒有深入到 V8 底層的源碼。通過本文可以對 V8 的編譯原理有一個感性的認知,同時也建議大家可以使用 TypeScript,它确實能在一定程度上對 JavaScript 代碼的編寫産生更好的指導作用。

繼續閱讀