詳解JavaScript引擎V8執(zhí)行流程
一、V8來源
V8的名字來源于汽車的“V型8缸發(fā)動機”(V8發(fā)動機)。V8發(fā)動機主要是美國發(fā)展起來,因為馬力十足而廣為人知。V8引擎的命名是Google向用戶展示它是一款強力并且高速的JavaScript引擎。
V8未誕生之前,早期主流的JavaScript引擎是JavaScriptCore引擎。JavaScriptCore是主要服務于Webkit瀏覽器內核,他們都是由蘋果公司開發(fā)并開源出來。據說Google是不滿意JavaScriptCore和Webkit的開發(fā)速度和運行速度,Google另起爐灶開發(fā)全新的JavaScript引擎和瀏覽器內核引擎,所以誕生了V8和Chromium兩大引擎,到現在已經是最受歡迎的瀏覽器相關軟件。
二、V8的服務對象
V8是依托Chrome發(fā)展起來的,后面確不局限于瀏覽器內核。發(fā)展至今V8應用于很多場景,例如流行的nodejs,weex,快應用,早期的RN。
三、V8的早期架構
V8引擎的誕生帶著使命而來,就是要在速度和內存回收上進行革命的。JavaScriptCore的架構是采用生成字節(jié)碼的方式,然后執(zhí)行字節(jié)碼。Google覺得JavaScriptCore這套架構不行,生成字節(jié)碼會浪費時間,不如直接生成機器碼快。所以V8在前期的架構設計上是非常激進的,采用了直接編譯成機器碼的方式。后期的實踐證明Google的這套架構速度是有改善,但是同時也造成了內存消耗問題??梢钥聪耉8的初期流程圖:

早期的V8有Full-Codegen和Crankshaft兩個編譯器。V8 首先用 Full-Codegen把所有的代碼都編譯一次,生成對應的機器碼。JS在執(zhí)行的過程中,V8內置的Profiler篩選出熱點函數并且記錄參數的反饋類型,然后交給 Crankshaft 來進行優(yōu)化。所以Full-Codegen本質上是生成的是未優(yōu)化的機器碼,而Crankshaft生成的是優(yōu)化過的機器碼。
四、V8早期架構的缺陷
隨著版本的引進,網頁的復雜化,V8也漸漸的暴露出了自己架構上的缺陷:
- Full-Codegen編譯直接生成機器碼,導致內存占用大
- Full-Codegen編譯直接生成機器碼,導致編譯時間長,導致啟動速度慢
- Crankshaft 無法優(yōu)化try,catch和finally等關鍵字劃分的代碼塊
- Crankshaft新加語法支持,需要為此編寫適配不同的Cpu架構代碼
五、V8的現有架構
為了解決上述缺點,V8采用JavaScriptCore的架構,生成字節(jié)碼。這里是不是感覺Google又繞回來了。V8采用生成字節(jié)碼的方式,整體流程如下圖:

Ignition是V8的解釋器,背后的原始動機是減少移動設備上的內存消耗。在Ignition之前,V8的Full-codegen基線編譯器生成的代碼通常占據Chrome整體JavaScript堆的近三分之一。這為Web應用程序的實際數據留下了更少的空間。
Ignition的字節(jié)碼可以直接用TurboFan生成優(yōu)化的機器代碼,而不必像Crankshaft那樣從源代碼重新編譯。Ignition的字節(jié)碼在V8中提供了更清晰且更不容易出錯的基線執(zhí)行模型,簡化了去優(yōu)化機制,這是V8 自適應優(yōu)化的關鍵特性。最后,由于生成字節(jié)碼比生成Full-codegen的基線編譯代碼更快,因此激活Ignition通常會改善腳本啟動時間,從而改善網頁加載。
TurboFan是V8的優(yōu)化編譯器,TurboFan項目最初于2013年底啟動,旨在解決Crankshaft的缺點。Crankshaft只能優(yōu)化JavaScript語言的子集。例如,它不是設計用于使用結構化異常處理優(yōu)化JavaScript代碼,即由JavaScript的try,catch和finally關鍵字劃分的代碼塊。很難在Crankshaft中添加對新語言功能的支持,因為這些功能幾乎總是需要為九個支持的平臺編寫特定于體系結構的代碼。
采用新架構后的優(yōu)勢
不同架構下V8的內存對比,如圖:

結論:可以明顯看出Ignition+TurboFan架構比Full-codegen+Crankshaft架構內存降低一半多。
不同架構網頁速度提升對比,如圖:

結論:可以明顯看出Ignition+TurboFan架構比Full-codegen+Crankshaft架構70%網頁速度是有提升的。
接下來我們大致的講解下現有架構的每個流程:
六、V8的詞法分析和語法分析
學過編譯原理的同學可以知道,JS文件只是一個源碼,機器是無法執(zhí)行的,詞法分析就是把源碼的字符串分割出來,生成一系列的token,如下圖可知不同的字符串對應不同的token類型。

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

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

V8 Parse階段后,接下來就是根據抽象語法樹生成字節(jié)碼。如下圖可以看出add函數生成對應的字節(jié)碼:

BytecodeGenerator類的作用是根據抽象語法樹生成對應的字節(jié)碼,不同的node會對應一個字節(jié)碼生成函數,函數開頭為Visit****。如下圖+號對應的函數字節(jié)碼生成:

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字節(jié)碼
}
}
上述可知有個源碼位置記錄,然后下圖可知源碼和字節(jié)碼位置的對應關系:

生成字節(jié)碼,那字節(jié)碼如何執(zhí)行的呢?接下來講解下:
八、字節(jié)碼
首先說下V8字節(jié)碼:
每個字節(jié)碼指定其輸入和輸出作為寄存器操作數
Ignition 使用registers寄存器 r0,r1,r2... 和累加器寄存器(accumulator register)
registers寄存器:函數參數和局部變量保存在用戶可見的寄存器中
累加器:是非用戶可見寄存器,用于保存中間結果
如下圖ADD字節(jié)碼:

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








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


例如ADD字節(jié)碼對應的處理函數是(當執(zhí)行ADD字節(jié)碼時候,會調用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(); // 處理下一條字節(jié)碼
}
其實到此JS代碼就已經執(zhí)行完成了。在執(zhí)行過程中,發(fā)現有熱點函數,V8會啟用Turbofan進行優(yōu)化編譯,直接生成機器碼,所以接下來講解下Turbofan優(yōu)化編譯器:
九、Turbofan
Turbofan是根據字節(jié)碼和熱點函數反饋類型生成優(yōu)化后的機器碼,Turbofan很多優(yōu)化過程,基本和編譯原理的后端優(yōu)化差不多,采用的sea-of-node。

add函數優(yōu)化:
function add(x, y) {
return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);
V8是有函數可以直接調用指定優(yōu)化哪個函數,執(zhí)行%OptimizeFunctionOnNextCall主動調用Turbofan優(yōu)化add函數,根據上次調用的參數反饋優(yōu)化add函數,很明顯這次的反饋是整型數,所以turbofan會根據參數是整型數進行優(yōu)化直接生成機器碼,下次函數調用直接調用優(yōu)化好的機器碼。(注意執(zhí)行V8需要加上 --allow-natives-syntax,OptimizeFunctionOnNextCall為內置函數,只有加上 --allow-natives-syntax,JS才能調用內置函數 ,否則執(zhí)行會報錯)。
JS的add函數生成對應的機器碼如下:

這里會涉及small interger小整數概念,可以查看這篇文章https://zhuanlan.zhihu.com/p/82854566
如果把add函數的傳入參數改成字符
function add(x, y) {
return x+y;
}
add(1, 2);
%OptimizeFunctionOnNextCall(add);
add(1, 2);
優(yōu)化后的add函數生成對應的機器碼如下:

對比上面兩圖,add函數傳入不同的參數,經過優(yōu)化生成不同的機器碼。
如果傳入的是整型,則本質上是直接調用add匯編指令
如果傳入的是字符串,則本質上是調用V8的內置Add函數
到此V8的整體執(zhí)行流程就結束了。
以上就是詳解JavaScript引擎V8執(zhí)行流程的詳細內容,更多關于JavaScript引擎V8的資料請關注腳本之家其它相關文章!
相關文章
JavaScript中變量提升導致未定義(undefined)的問題及解決方法
在 JavaScript 中,變量提升(Hoisting)是一個相對常見的行為,尤其是當你遇到 undefined 錯誤時,本文將詳細探討變量提升的概念、其對代碼執(zhí)行的影響以及如何避免因為變量提升而導致 undefined 的問題,需要的朋友可以參考下2024-09-09
DD_belatedPNG,IE6下PNG透明解決方案(國外)
今天介紹DD_belatedPNG,只需要一個理由,就是它支持backgrond-position與background-repeat.這是其他js插件不具備的.2010-12-12
JavaScript類型系統(tǒng)之基本數據類型與包裝類型
javascript的數據類型可以分為兩種:原始類型和引用類型,在此文給大家提到。本文主要給大家介紹javascript類型系統(tǒng)之基本數據類型與包裝類型,涉及到js 基本類型 包裝類相關知識,本文介紹的詳細,具有參考借鑒價值,對本文感興趣的朋友一起學習吧2016-01-01
JavaScript中Number.isNaN 和 isNaN 的區(qū)別詳解
本文和大家分享一個前幾天寫代碼踩的坑,筆者在業(yè)務邏輯中需要對一個值進行NaN的判斷,由于筆者的不嚴謹,使用了isNaN,從而引起B(yǎng)ug,也正是因為這個,筆者才知道了isNaN和Number.isNaN的區(qū)別,所以本文就和大家聊聊它們的區(qū)別2023-09-09
JavaScript編碼風格精選指南(編寫可維護的代碼規(guī)范)
javascript編碼規(guī)范能夠增強代碼的簡潔性、可讀性、可擴展性,項目做到后期,每修改一次,所耗費的成本就越高,編碼規(guī)范能節(jié)省這樣的成本,并且能很好拓展升級原有系統(tǒng)功能,javascript編碼規(guī)范也是開源社區(qū)大家約定俗成的規(guī)則!2024-06-06
學習javascript面向對象 javascript實現繼承的方式
這篇文章主要為大家介紹了javascript實現繼承的方式,學習javascript面向對象,感興趣的小伙伴們可以參考一下2016-01-01

