使用 D8 分析 javascript 如何被 V8 引擎優化的

justjavac發表於2017-02-08

在上一篇文章中我們講了如何使用 GN 編譯 V8 原始碼,文章最後編譯完成的可執行檔案並不是 V8,而是 D8。這篇我們講一下如何使用 D8 除錯 javascript 程式碼。

如果沒有 d8,可以使用 node 代替。

新建檔案 add-of-ints.js,輸入以下內容:

function add(obj) {
    return obj.prop + obj.prop;
}

const length = 1000 * 1000;

const o = { prop: 1 };

for (let i = 0; i < length; i++) {
    add(o);

}複製程式碼

執行:

d8 --trace-opt-verbose add-of-ints.js
或
node --trace-opt-verbose add-of-ints.js複製程式碼

輸出結果為:

使用 D8 分析 javascript 如何被 V8 引擎優化的

從輸出結果我們可以看到 add 函式被編譯器優化了,並且解釋了優化的原因。ICs 是 inline caches 的縮寫,內聯快取是一種很常見的優化技術,這段簡短的程式碼被 V8 引擎優化了兩次,但是原因卻不同。

  • 第一次優化的原因是 small function,add 函式是小函式,為了減小函式呼叫的開銷,V8 引擎對 add 做了優化。

  • 第二次的原因是 hot and stable,我在知乎另一個問題中曾說過,V8 有兩個編譯器,一個通用編譯器,負責將 javascript 程式碼編譯為機器碼,另一個是優化編譯器。從上面的輸出可以看出 V8 使用的優化編譯器引擎是 Crankshaft。Crankshaft 負責找出經常被呼叫的程式碼,做內聯快取優化,後面的資訊進一步說明了這個情況:ICs with typeinfo: 7/7 (100%), generic ICs: 0/7 (0%)。

在此再糾正之前的 2 個問題。

一個是 V8 沒有直譯器,只有編譯器,程式碼是直接編譯成機器嗎執行的,這是之前的 V8,而網路上關於 V8 的文章也大多比較老舊。這幾天為了閱讀 V8 原始碼檢視了網上很多關於 V8 的論文和文章,發現 V8 已經引進了直譯器。因為 V8 不僅僅可以優化程式碼,還可以去優化(deopt),引入直譯器可以省去一些程式碼的重編譯時間,另一個原因是直譯器不僅僅可以解釋 javascript 程式碼,還可以解釋 asm 或者其他二進位制中間碼。

另一個錯誤就是關於 V8 優化的,之前寫過 JavaScript 函數語言程式設計存在效能問題麼? 中道:

永遠不可能被優化的有:

  • Functions that contain a debugger statement
  • Functions that call literally eval()
  • Functions that contain a with statement

這個也是之前的文章,是以 Crankshaft 引擎為標準得出的結論。而 V8 已經開發了新的優化引擎——TurboFan

我們再建立另一個檔案 add-of-mixed.js,輸入:

// flag: --trace-opt-verbose

function add(obj) {
    return obj.prop + obj.prop;
}

var length = 1000 * 1000;

var objs = new Array(length);

var i = 0;

for (i = 0; i < length; i++) {
    objs[i] = Math.random();
}

var a = { prop: 'a' };
var b = { prop: 1 };

for (i = 0; i < length; i++) {
    add(objs[i] > 0.5 ? a : b);

}複製程式碼

執行:

d8 --trace-opt-verbose add-of-mixed.js
或
node --trace-opt-verbose add-of-mixed.js複製程式碼

輸出結果為:

使用 D8 分析 javascript 如何被 V8 引擎優化的

使用 D8 分析 javascript 如何被 V8 引擎優化的

可以看到這段程式碼能不能做內聯快取優化全看 RP(人品) 了。

我們再使用 --trace-opt --trace-deopt 引數看看 V8 引擎如何去優化

新建檔案 add-of-mixed-dep.js,輸入:

// flags: --trace-opt --trace-deopt

function add(obj) {
    return obj.prop + obj.prop;
}

var length = 10000;
var i = 0;
var a = { prop: 'a' };
var b = { prop: 1 };

for (i = 0; i < length; i++) {
    add(i !== 8000 ? a : b);

}複製程式碼

執行:

d8 --trace-opt --trace-deopt add-of-mixed-dep.js
或
node --trace-opt --trace-deopt add-of-mixed-dep.js複製程式碼

結果為:

使用 D8 分析 javascript 如何被 V8 引擎優化的

V8 引擎內部使用 Hidden Classes 來表示 Object,關於 Hidden Classes 的文章已經很多了,我就不累述了。

執行 d8 --help 可以檢視所有的 d8 命令列引數。如果使用 node,直接執行 node --help 輸出的是 node 的命令列引數,如果想檢視 V8 的,需要使用 node --v8-options

後面章節會介紹 V8 的 GC(命令列引數 --trace-gc)以及最有意思的 --allow-natives-syntax

推薦閱讀一下 V8 的 bailout-reason.h 原始碼,這是一個 C++ 的標頭檔案,裡面幾乎沒有任何程式碼邏輯,定義了所有 javascript 程式碼不能被 V8 引擎優化的原因,比如:

"Array index constant value too big"
"eval"
"ForOfStatement"
"Too many parameters"
"WithStatement"
……複製程式碼

後面章節介紹的 --allow-natives-syntax 相關 C++ 標頭檔案是 runtime.h,通過 --allow-natives-syntax 引數可以在 javascript 中使用 V8 的執行時函式。我們在之前的文章中已經使用過了,例如 HasFastProperties

參考文章:

相關文章