天天看點

JavaScript 引擎 V8 執行流程概述

本文主要講解的是V8的技術,是V8的入門篇,主要目的是了解V8的内部機制,希望對前端,快應用,浏覽器,以及nodejs同學有些幫助。這裡不涉及到如何編寫優秀的前端,隻是對JS内部引擎技術的講解。

一、V8來源

JavaScript 引擎 V8 執行流程概述

V8的名字來源于汽車的“V型8缸發動機”(V8發動機)。V8發動機主要是美國發展起來,因為馬力十足而廣為人知。V8引擎的命名是Google向使用者展示它是一款強力并且高速的JavaScript引擎。

V8未誕生之前,早期主流的JavaScript引擎是JavaScriptCore引擎。JavaScriptCore是主要服務于Webkit浏覽器核心,他們都是由蘋果公司開發并開源出來。據說Google是不滿意JavaScriptCore和Webkit的開發速度和運作速度,Google另起爐竈開發全新的JavaScript引擎和浏覽器核心引擎,是以誕生了V8和Chromium兩大引擎,到現在已經是最受歡迎的浏覽器相關軟體。

二、V8的服務對象

V8是依托Chrome發展起來的,後面确不局限于浏覽器核心。發展至今V8應用于很多場景,例如流行的nodejs,weex,快應用,早期的RN。

三、V8的早期架構

V8引擎的誕生帶着使命而來,就是要在速度和記憶體回收上進行革命的。JavaScriptCore的架構是采用生成位元組碼的方式,然後執行位元組碼。Google覺得JavaScriptCore這套架構不行,生成位元組碼會浪費時間,不如直接生成機器碼快。是以V8在前期的架構設計上是非常激進的,采用了直接編譯成機器碼的方式。後期的實踐證明Google的這套架構速度是有改善,但是同時也造成了記憶體消耗問題。可以看下V8的初期流程圖:

JavaScript 引擎 V8 執行流程概述

早期的V8有Full-Codegen和Crankshaft兩個編譯器。V8 首先用 Full-Codegen把所有的代碼都編譯一次,生成對應的機器碼。JS在執行的過程中,V8内置的Profiler篩選出熱點函數并且記錄參數的回報類型,然後交給 Crankshaft 來進行優化。是以Full-Codegen本質上是生成的是未優化的機器碼,而Crankshaft生成的是優化過的機器碼。

四、V8早期架構的缺陷

随着版本的引進,網頁的複雜化,V8也漸漸的暴露出了自己架構上的缺陷:

  1. Full-Codegen編譯直接生成機器碼,導緻記憶體占用大
  2. Full-Codegen編譯直接生成機器碼,導緻編譯時間長,導緻啟動速度慢
  3. Crankshaft 無法優化try,catch和finally等關鍵字劃分的代碼塊
  4. Crankshaft新加文法支援,需要為此編寫适配不同的Cpu架構代碼

五、V8的現有架構

為了解決上述缺點,V8采用JavaScriptCore的架構,生成位元組碼。這裡是不是感覺Google又繞回來了。V8采用生成位元組碼的方式,整體流程如下圖:

JavaScript 引擎 V8 執行流程概述

Ignition是V8的解釋器,背後的原始動機是減少移動裝置上的記憶體消耗。在Ignition之前,V8的Full-codegen基線編譯器生成的代碼通常占據Chrome整體JavaScript堆的近三分之一。這為Web應用程式的實際資料留下了更少的空間。

Ignition的位元組碼可以直接用TurboFan生成優化的機器代碼,而不必像Crankshaft那樣從源代碼重新編譯。Ignition的位元組碼在V8中提供了更清晰且更不容易出錯的基線執行模型,簡化了去優化機制,這是V8 自适應優化的關鍵特性。最後,由于生成位元組碼比生成Full-codegen的基線編譯代碼更快,是以激活Ignition通常會改善腳本啟動時間,進而改善網頁加載。

TurboFan是V8的優化編譯器,TurboFan項目最初于2013年底啟動,旨在解決Crankshaft的缺點。Crankshaft隻能優化JavaScript語言的子集。例如,它不是設計用于使用結構化異常處理優化JavaScript代碼,即由JavaScript的try,catch和finally關鍵字劃分的代碼塊。很難在Crankshaft中添加對新語言功能的支援,因為這些功能幾乎總是需要為九個支援的平台編寫特定于體系結構的代碼。

采用新架構後的優勢

不同架構下V8的記憶體對比,如圖:

JavaScript 引擎 V8 執行流程概述

結論:可以明顯看出Ignition+TurboFan架構比Full-codegen+Crankshaft架構記憶體降低一半多。

不同架構網頁速度提升對比,如圖:

JavaScript 引擎 V8 執行流程概述

結論:可以明顯看出Ignition+TurboFan架構比Full-codegen+Crankshaft架構70%網頁速度是有提升的。

接下來我們大緻的講解下現有架構的每個流程:

六、V8的詞法分析和文法分析

學過編譯原理的同學可以知道,JS檔案隻是一個源碼,機器是無法執行的,詞法分析就是把源碼的字元串分割出來,生成一系列的token,如下圖可知不同的字元串對應不同的token類型。

JavaScript 引擎 V8 執行流程概述

詞法分析完後,接下來的階段就是進行文法分析。文法分析文法分析的輸入就是詞法分析的輸出,輸出是AST抽象文法樹。當程式出現文法錯誤的時候,V8在文法分析階段抛出異常。

JavaScript 引擎 V8 執行流程概述

七、V8 AST抽象文法樹

下圖是一個add函數的抽象文法樹資料結構

JavaScript 引擎 V8 執行流程概述

V8 Parse階段後,接下來就是根據抽象文法樹生成位元組碼。如下圖可以看出add函數生成對應的位元組碼:

JavaScript 引擎 V8 執行流程概述

BytecodeGenerator類的作用是根據抽象文法樹生成對應的位元組碼,不同的node會對應一個位元組碼生成函數,函數開頭為Visit**。如下圖+号對應的函數位元組碼生成:

JavaScript 引擎 V8 執行流程概述
void BytecodeGenerator::VisitArithmeticExpression(BinaryOperation* expr) {
  FeedbackSlot slot = feedback_spec()->AddBinaryOpICSlot();
  Expression* subexpr;
  Smi* literal;
  
  if (expr->IsSmiLiteralOperation(&subexpr, &literal)) {
    VisitForAccumulatorValue(subexpr);
    builder()->SetExpressionPosition(expr);
    builder()->BinaryOperationSmiLiteral(expr->op(), literal,
                                         feedback_index(slot));
  } else {
    Register lhs = VisitForRegisterValue(expr->left());
    VisitForAccumulatorValue(expr->right());
    builder()->SetExpressionPosition(expr);  //  儲存源碼位置 用于調試
    builder()->BinaryOperation(expr->op(), lhs, feedback_index(slot)); //  生成Add位元組碼
  }
}           

上述可知有個源碼位置記錄,然後下圖可知源碼和位元組碼位置的對應關系:

JavaScript 引擎 V8 執行流程概述

生成位元組碼,那位元組碼如何執行的呢?接下來講解下:

八、位元組碼

首先說下V8位元組碼:

  1.  每個位元組碼指定其輸入和輸出作為寄存器操作數
  2.  Ignition 使用registers寄存器 r0,r1,r2... 和累加器寄存器(accumulator register)
  3.  registers寄存器:函數參數和局部變量儲存在使用者可見的寄存器中
  4. 累加器:是非使用者可見寄存器,用于儲存中間結果

如下圖ADD位元組碼:

JavaScript 引擎 V8 執行流程概述

位元組碼執行

下面一系列圖表示每個位元組碼執行時,對應寄存器和累加器的變化,add函數傳入10,20的參數,最終累加器傳回的結果是50。

JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述

每個位元組碼對應一個處理函數,位元組碼處理程式儲存的位址在dispatch_table_中。執行位元組碼時會調用到對應的位元組碼處理程式進行執行。Interpreter類成員dispatch_table_儲存了每個位元組碼的處理程式位址。

JavaScript 引擎 V8 執行流程概述
JavaScript 引擎 V8 執行流程概述

例如ADD位元組碼對應的處理函數是(當執行ADD位元組碼時候,會調用InterpreterBinaryOpAssembler類):

IGNITION_HANDLER(Add, InterpreterBinaryOpAssembler) {
   BinaryOpWithFeedback(&BinaryOpAssembler::Generate_AddWithFeedback);
}
  
void BinaryOpWithFeedback(BinaryOpGenerator generator) {
    Node* reg_index = BytecodeOperandReg(0);
    Node* lhs = LoadRegister(reg_index);
    Node* rhs = GetAccumulator();
    Node* context = GetContext();
    Node* slot_index = BytecodeOperandIdx(1);
    Node* feedback_vector = LoadFeedbackVector();
    BinaryOpAssembler binop_asm(state());
    Node* result = (binop_asm.*generator)(context, lhs, rhs, slot_index,                            
feedback_vector, false);
    SetAccumulator(result);  // 将ADD計算的結果設定到累加器中
    Dispatch(); // 處理下一條位元組碼
  
}           

其實到此JS代碼就已經執行完成了。在執行過程中,發現有熱點函數,V8會啟用Turbofan進行優化編譯,直接生成機器碼。是以接下來講解下Turbofan優化編譯器:

九、Turbofan

Turbofan是根據位元組碼和熱點函數回報類型生成優化後的機器碼,Turbofan很多優化過程,基本和編譯原理的後端優化差不多,采用的sea-of-node。

JavaScript 引擎 V8 執行流程概述

add函數優化:

function add(x, y) {
  return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);           

V8是有函數可以直接調用指定優化哪個函數,執行%OptimizeFunctionOnNextCall主動調用Turbofan優化add函數,根據上次調用的參數回報優化add函數,很明顯這次的回報是整型數,是以turbofan會根據參數是整型數進行優化直接生成機器碼,下次函數調用直接調用優化好的機器碼。(注意執行V8需要加上 --allow-natives-syntax,OptimizeFunctionOnNextCall為内置函數,隻有加上 --allow-natives-syntax,JS才能調用内置函數 ,否則執行會報錯)。

JS的add函數生成對應的機器碼如下:

JavaScript 引擎 V8 執行流程概述

這裡會涉及small interger小整數概念,可以檢視這篇文章

https://zhuanlan.zhihu.com/p/82854566

如果把add函數的傳入參數改成字元

function add(x, y) {
  return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);           

優化後的add函數生成對應的機器碼如下:

JavaScript 引擎 V8 執行流程概述

對比上面兩圖,add函數傳入不同的參數,經過優化生成不同的機器碼。

如果傳入的是整型,則本質上是直接調用add彙編指令

如果傳入的是字元串,則本質上是調用V8的内置Add函數

到此V8的整體執行流程就結束了。文章中可能存在了解不正确的地方敬請指出。

  • 參考文章
  1. https://v8.dev/docs
  2. https://docs.google.com/presentation/d/1HgDDXBYqCJNasBKBDf9szap1j4q4wnSHhOYpaNy5mHU/edit#slide=id.g17d335048f_1_1105
  3. https://docs.google.com/presentation/d/1Z9iIHojKDrXvZ27gRX51UxHD-bKf1QcPzSijntpMJBM/edit#slide=id.p

繼續閱讀