1. 程式人生 > >【NodeJS】對於V8引擎的一點認識..

【NodeJS】對於V8引擎的一點認識..

JavaScript程式碼的編譯與優化

  Node可以看作是JavaScript的執行時環境。一方面,它提供了多種可呼叫的API,如讀寫檔案、網路請求、系統資訊等。另一方面,因為CPU執行的是機器碼,它還負責將JavaScript程式碼解釋成機器指令序列執行,這部分工作是由V8引擎完成。

即時編譯

  V8採用即時編譯技術(JIT),直接將JavaScript程式碼編譯成本地平臺的機器碼。巨集觀上看,其步驟為JavaScript原始碼—>抽象語法樹—>本地機器碼,並且後一個步驟只依賴前一個步驟。
  這與其他直譯器不同,例如Java語言需要先將原始碼編譯成位元組碼,然後給JVM解釋執行,JVM根據優化策略,執行過程中有選擇地將一部分位元組碼編譯成本地機器碼。
  V8不生成中間程式碼,一步到位,編譯成機器碼,CPU就開始執行了。比起生成中間碼解釋執行的方式,V8的策略省去了一個步驟,程式會更早地開始執行。並且執行編譯好的機器指令,也比解釋執行中間碼的速度更快。不足的是,缺少位元組碼這個中間表示,使得程式碼優化變得更困難。

隱藏類

  首先我們看一下C++/Java這種靜態型別語言的每一個變數,都有一個唯一確定的型別。因為有型別資訊,一個物件包含哪些成員和這些成員在物件中的偏移量等資訊,編譯階段就可確定,執行時CPU只需要用物件首地址 —— 在C++中是this指標,加上成員在物件內部的偏移量即可訪問內部成員。這些訪問指令在編譯階段就生成了。
  但對於JavaScript這種動態語言,變數在執行時可以隨時由不同型別的物件賦值,並且物件本身可以隨時新增刪除成員。訪問物件屬性需要的資訊完全由執行時決定。為了實現按照索引的方式訪問成員,V8“悄悄地”給執行中的物件分了類,在這個過程中產生了一種V8內部的資料結構,即隱藏類。隱藏類本身是一個物件。
  隱藏類起到給物件分組的作用。同一組的物件,具有相同的成員名稱。隱藏類記錄了成員名稱和偏移量,根據這些資訊,V8能夠按照物件首地址+偏移量訪問成員變數。

內聯快取

  上面講到,藉助隱藏類,可以使用陣列索引的方式存取物件成員。但成員的索引值是以雜湊表的方式儲存在隱藏類中。如果每次訪問屬性都搜尋隱藏類的雜湊表,那麼這種使用偏移量的方式不會帶來任何好處。
  內斂快取是基於程式執行的區域性性原理,動態生成使用索引查詢的程式碼。下一次存取成員變數就不必再去搜尋雜湊表。

優化回退

  V8 為了進一步提升JavaScript程式碼的執行效率,使用了Crankshaft編譯器生成更高效的機器碼。程式在執行時,V8會採集JavaScript程式碼執行資料。當V8發現某函式執行頻繁,就將其標記為熱點函式。針對熱點函式,V8的策略較為樂觀,傾向於認為此函式比較穩定,型別已經確定,於是呼叫Crankshaft編譯器,生成更高效的機器碼。後面的執行中,萬一遇到型別變化,V8採取將JavaScript函式回退到優化前的較一般的情況。

function add(a, b){
    return a + b
}
for(var i=0; i<10000; ++i){
    add(i, i);
}
add('a', 'b');

  上述程式碼在執行for迴圈的過程中,每次呼叫add()函式,傳入的引數是整型,執行一定次數後,V8可能把這個函式標記為熱點函式,並根據每次執行傳入的引數預測,此函式的引數a、b為整型。於是呼叫Crankshaft編譯器生成相應的程式碼。但當迴圈退出,執行字串想加時,V8只好將函式回退到一般狀態。回退過程就是根據函式原始碼,生成相應的語法時,然後編譯成一般形式的機器碼。可以預見這個過程是比較耗時的,並且放棄了優化後的程式碼去執行一般形式的程式碼,因此要儘量避免觸發。

那麼再來看一個例子:

// 片段 1
var person = {
    add: function(a, b){
        return a + b;
    }
};
obj.name = 'li';

// 片段 2
var person = {
    add: function(a, b){
        return a + b;
    },

    name: 'li'
};

  以上程式碼實現的功能相同,都是定義了一個物件,這個物件具有一個屬性name和一個方法add()。但使用片段2的方式效率更高。片段1給物件obj添加了一個屬性name,這會造成隱藏類的派生。給物件動態地新增和刪除屬性都會派生新的隱藏類。假如物件的add函式已經被優化,生成了更高效的程式碼,則因為新增或刪除屬性,這個改變後的物件無法使用優化後的程式碼。
  上面的優化回退的例子也啟示我們,函式內部的引數型別越確定,V8越能夠生成優化後的程式碼。我們也要避免優化回退,例如可以再編寫一個專門針對字串想加的函式,而不是一個函式同時處理整型和字串。