JavaScript 引擎 V8 執行流程概述

本文首發於 vivo互聯網技術 微信公衆號 
連接:https://mp.weixin.qq.com/s/t__Jqzg1rbTlsCHXKMwh6A
做者:賴勇高前端

本文主要講解的是V8的技術,是V8的入門篇,主要目的是瞭解V8的內部機制,但願對前端,快應用,瀏覽器,以及nodejs同窗有些幫助。這裏不涉及到如何編寫優秀的前端,只是對JS內部引擎技術的講解。node

1、V8來源

V8的名字來源於汽車的「V型8缸發動機」(V8發動機)。V8發動機主要是美國發展起來,由於馬力十足而廣爲人知。V8引擎的命名是Google向用戶展現它是一款強力而且高速的JavaScript引擎。後端

V8未誕生以前,早期主流的JavaScript引擎是JavaScriptCore引擎。JavaScriptCore是主要服務於Webkit瀏覽器內核,他們都是由蘋果公司開發並開源出來。聽說Google是不滿意JavaScriptCore和Webkit的開發速度和運行速度,Google另起爐竈開發全新的JavaScript引擎和瀏覽器內核引擎,因此誕生了V8和Chromium兩大引擎,到如今已是最受歡迎的瀏覽器相關軟件。瀏覽器

2、V8的服務對象

V8是依託Chrome發展起來的,後面確不侷限於瀏覽器內核。發展至今V8應用於不少場景,例如流行的nodejs,weex,快應用,早期的RN。微信

3、V8的早期架構

V8引擎的誕生帶着使命而來,就是要在速度和內存回收上進行革命的。JavaScriptCore的架構是採用生成字節碼的方式,而後執行字節碼。Google以爲JavaScriptCore這套架構不行,生成字節碼會浪費時間,不如直接生成機器碼快。因此V8在前期的架構設計上是很是激進的,採用了直接編譯成機器碼的方式。後期的實踐證實Google的這套架構速度是有改善,可是同時也形成了內存消耗問題。能夠看下V8的初期流程圖:weex

早期的V8有Full-Codegen和Crankshaft兩個編譯器。V8 首先用 Full-Codegen把全部的代碼都編譯一次,生成對應的機器碼。JS在執行的過程當中,V8內置的Profiler篩選出熱點函數而且記錄參數的反饋類型,而後交給 Crankshaft 來進行優化。因此Full-Codegen本質上是生成的是未優化的機器碼,而Crankshaft生成的是優化過的機器碼。數據結構

4、V8早期架構的缺陷

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

  1. Full-Codegen編譯直接生成機器碼,致使內存佔用大
  2. Full-Codegen編譯直接生成機器碼,致使編譯時間長,致使啓動速度慢
  3. Crankshaft 沒法優化try,catch和finally等關鍵字劃分的代碼塊
  4. Crankshaft新加語法支持,須要爲此編寫適配不一樣的Cpu架構代碼

5、V8的現有架構

爲了解決上述缺點,V8採用JavaScriptCore的架構,生成字節碼。這裏是否是感受Google又繞回來了。V8採用生成字節碼的方式,總體流程以下圖:ide

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的內存對比,如圖:

結論:能夠明顯看出Ignition+TurboFan架構比Full-codegen+Crankshaft架構內存下降一半多。

不一樣架構網頁速度提高對比,如圖:

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

接下來咱們大體的講解下現有架構的每一個流程:

6、V8的詞法分析和語法分析

學過編譯原理的同窗能夠知道,JS文件只是一個源碼,機器是沒法執行的,詞法分析就是把源碼的字符串分割出來,生成一系列的token,以下圖可知不一樣的字符串對應不一樣的token類型。

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

7、V8 AST抽象語法樹

下圖是一個add函數的抽象語法樹數據結構

V8 Parse階段後,接下來就是根據抽象語法樹生成字節碼。以下圖能夠看出add函數生成對應的字節碼:

BytecodeGenerator類的做用是根據抽象語法樹生成對應的字節碼,不一樣的node會對應一個字節碼生成函數,函數開頭爲Visit**。以下圖+號對應的函數字節碼生成:

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字節碼
  }
}

上述可知有個源碼位置記錄,而後下圖可知源碼和字節碼位置的對應關係:

生成字節碼,那字節碼如何執行的呢?接下來說解下:

8、字節碼

首先說下V8字節碼:

  1.  每一個字節碼指定其輸入和輸出做爲寄存器操做數
  2.  Ignition 使用registers寄存器 r0,r1,r2... 和累加器寄存器(accumulator register)
  3.  registers寄存器:函數參數和局部變量保存在用戶可見的寄存器中
  4. 累加器:是非用戶可見寄存器,用於保存中間結果

以下圖ADD字節碼:

字節碼執行

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

每一個字節碼對應一個處理函數,字節碼處理程序保存的地址在dispatch_table_中。執行字節碼時會調用到對應的字節碼處理程序進行執行。Interpreter類成員dispatch_table_保存了每一個字節碼的處理程序地址。

例如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優化編譯器:

9、Turbofan

Turbofan是根據字節碼和熱點函數反饋類型生成優化後的機器碼,Turbofan不少優化過程,基本和編譯原理的後端優化差很少,採用的sea-of-node。

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函數生成對應的機器碼以下:

這裏會涉及small interger小整數概念,能夠查看這篇文章https://zhuanlan.zhihu.com/p/...

若是把add函數的傳入參數改爲字符

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

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

對比上面兩圖,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
  4. https://zhuanlan.zhihu.com/p/82854566
相關文章
相關標籤/搜索