1. 程式人生 > 程式設計 >詳解JavaScript引擎V8執行流程

詳解JavaScript引擎V8執行流程

目錄
  • 一、V8來源
  • 二、V8的服務物件
  • 三、V8的早期架構
  • 四、V8早期架構的缺陷
  • 五、V8的現有架構
  • 六、V8的詞法分析和語法分析
  • 七、V8 AST抽象語法樹
  • 八、位元組碼
  • 九、Turbofan

一、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也漸漸的暴露出了自己架構上的缺陷:

  • Full-Codegen編譯直接生成機器碼,導致記憶體佔用大
  • Full-Codegen編譯直接生成機器碼,導致編譯時間長,導致啟動速度慢
  • Crankshaft 無法優化try,catch和finally等關鍵字劃分的程式碼塊
  • 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位元組碼:

每個位元組碼指定其輸入和輸出作為暫存器運算元

Ignition 使用registers暫存器 r0,r1,r2... 和累加器暫存器(accumulator register)

registers暫存器:函式引數和區域性變數儲存在使用者可見的暫存器中

累加器:是非使用者可見暫存器,用於儲存中間結果

如下圖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,InterpreterBinaryOhttp://www.cppcns.compAssembler) {
   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,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是有函式可以直接呼叫指定優化哪個函式,執行%OptimizeFunct程式設計客棧ionOnNextCall主動呼叫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(xgUCpYn,2);

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

詳解JavaScript引擎V8執行流程

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

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

如果傳入的是字串,則本質上是呼叫V8的內建Add函式

到此V8的整體執行流程就結束了。

以上就是詳解JavaScript引擎V8執行流程的詳細內容,更多關於JavaScript引擎V8的資料請關注我們其它相關文章!