JavaScript引擎
我們寫的JavaScript代碼直接交給浏覽器或者Node執行時,底層的CPU是不認識的,也沒法執行。CPU只認識自己的指令集,指令集對應的是彙編代碼。寫彙編代碼是一件很痛苦的事情,比如,我們要計算N階乘的話,只需要7行的遞歸函數:
function factorial(N) {
if (N === 1) {
return 1;
} else {
return N * factorial(N – 1);
}
}
代碼邏輯也非常清晰,與階乘的數學定義完美吻合,哪怕不會寫代碼的人也能看懂。
但是,如果使用彙編語言來寫N階乘的話,要300+行代碼n-factorial.s:
V8:強大的JavaScript引擎
在爲數不多JavaScript引擎中,V8無疑是最流行的,Chrome與Node.js都使用了V8引擎,Chrome的市場占有率高達60%,而Node.js是JS後端編程的事實標准。國內的衆多浏覽器,其實都是基于Chromium浏覽器開發,而Chromium相當于開源版本的Chrome,自然也是基于V8引擎的。神奇的是,就連浏覽器界的獨樹一幟的Microsoft也投靠了Chromium陣營。另外,Electron是基于Node.js與Chromium開發桌面應用,也是基于V8的。
V8引擎是2008年發布的,它的命名靈感來自超級性能車的V8引擎,敢于這樣命名確實需要一些實力,它性能確實一直在穩步提高,下面是使用Speedometer benchmark的測試結果:
V8引擎的內部結構
V8是一個非常複雜的項目,使用cloc統計可知,它竟然有超過100萬行C++代碼。
V8由許多子模塊構成,其中這4個模塊是最重要的:
-
Parser:負責將JavaScript源碼轉換爲Abstract Syntax Tree (AST)
-
Ignition:interpreter,即解釋器,負責將AST轉換爲Bytecode,解釋執行Bytecode;同時收集TurboFan優化編譯所需的信息,比如函數參數的類型;
-
TurboFan:compiler,即編譯器,利用Ignitio所收集的類型信息,將Bytecode轉換爲優化的彙編代碼;
-
Orinoco:garbage collector,垃圾回收模塊,負責將程序不再需要的內存空間回收;
其中,Parser,Ignition以及TurboFan可以將JS源碼編譯爲彙編代碼,其流程圖如下:
Ignition:解釋器
生成的Bytecode其實挺簡單的:
-
使用LdaSmi命令將整數1保存到寄存器;
-
使用TestEqualStrict命令比較參數a0與1的大小;
-
如果a0與1相等,則JumpIfFalse命令不會跳轉,繼續執行下一行代碼;
-
如果a0與1不相等,則JumpIfFalse命令會跳轉到內存地址0x3541c2da1139
-
…
不難發現,Bytecode某種程度上就是彙編語言,只是它沒有對應特定的CPU,或者說它對應的是虛擬的CPU。這樣的話,生成Bytecode時簡單很多,無需爲不同的CPU生産不同的代碼。要知道,V8支持9種不同的CPU,引入一個中間層Bytecode,可以簡化V8的編譯流程,提高可擴展性。
如果我們在不同硬件上去生成Bytecode,會發現生成代碼的指令是一樣的:
TurboFan:編譯器
比起Bytecode,正真的彙編代碼可讀性差很多。而且,機器的CPU類型不一樣的話,生成的彙編代碼也不一樣。
這些彙編代碼就不用去管它了,因爲最重要的是理解TurboFan是如何優化所生成的彙編代碼的。我們可以通過add函數來梳理整個優化過程。
function add(x, y) {
return x + y;
}
add(1, 2);
add(3, 4);
add(5, 6);
add(“7”, “8”);
由于JS的變量是沒有類型的,所以add函數的參數可以是任意類型:Number、String、Boolean等,這就意味著add函數可能是數字相加(V8還會區分整數和浮點數),可能是字符串拼接,也可能是其他更複雜的操作。如果直接編譯的話,生成的代碼比如會有很多if…else分支,僞代碼如下:
if (isInteger(x) && isInteger(y)) {
// 整數相加
} else if (isFloat(x) && isFloat(y)) {
// 浮點數相加
} else if (isString(x) && isString(y)) {
// 字符串拼接
} else {
// 各種其他情況
}
我只寫了4個分支,實際上的分支其實更多,比如當參數類型不一致時還得進行類型轉換,大家不妨看看ECMASCript對加法是如何定義的:12.8.3The Addition Operator ( + )。
如果直接按照僞代碼去生成彙編代碼,那生成的代碼必然非常冗長,這樣會占用很多內存空間。
Ignition在執行add(1, 2)時,已經知道add函數的兩個參數都是整數,那麽TurboFan在編譯Bytecode時,就可以假定add函數的參數是整數,這樣可以極大地簡化生成的彙編代碼,僞代碼如下:
if (isInteger(x) && isInteger(y)) {
// 整數相加
} else {
// Deoptimization
}
當然這樣做也是有風險的,因爲如果add函數參數不是整數,那麽生成的彙編代碼也沒法執行,只能Deoptimize爲Bytecode來執行。
也就是說,如果TurboFan對add函數進行編譯優化的話,則add(3, 4)與add(3, 4)可以執行優化的彙編代碼,但是add(“7”, “8”)只能Deoptimize爲Bytecode來執行。
當然,TurboFan所做的也不只是根據類型信息來簡化代碼執行流程,它還會進行其他優化,比如減少冗余代碼等更複雜的事情。
由這個簡單的例子可知,如果我們的JS代碼中變量的類型變來變去,是會給V8引擎增加不少麻煩的,爲了提高性能,我們可以盡量不要去改變變量的類型。
對于性能要求比較高的項目,使用TypeScript也是不錯的選擇,理論上,如果嚴格遵守類型化的編程方式,也是可以提高性能的,類型化的代碼有利于V8引擎優化編譯的彙編代碼,當然這一點還需要測試數據來證明。
強大的垃圾回收功能是V8實現提高性能的關鍵之一,因爲它可以在避免影響JS代碼執行的情況下,同時回收內存空間,提高內存利用效率。
關于垃圾回收,我在JavaScript深入淺出第3課:什麽是垃圾回收算法?中有詳細介紹,這裏就不再贅述了。