javascript引擎——V8

經過上一篇文章,咱們知道了JavaScript引擎是執行JavaScript代碼的程序或解釋器,瞭解了JavaScript引擎的基本工做原理。咱們常常據說的JavaScript引擎就是V8引擎,這篇文章咱們就來認識一下V8引擎,咱們先來看一下除了V8引擎,還有哪些JS引擎:javascript

  • V8 開源
    由Google開發,用C++編寫。V8 最先被開發用以嵌入到 Google 的開源瀏覽器 Chrome 中,可是 V8 是一個能夠獨立的模塊,徹底能夠嵌入您本身的應用,著名的 Node.js( 一個異步的服務器框架,能夠在服務端使用 JavaScript 寫出高效的網絡服務器 ) 就是基於 V8 引擎的。
  • Rhino開源
     由Mozilla基金所管理,徹底用Java開發
  • JavaScriptCore 開源
    由蘋果公司爲Safari開發
  • SpiderMonkey
    第一個JavaScript引擎,最先用在Netscape Navigator上,如今用在Firefox上
  • KJS
    KDE的引擎,最初由Harri Porten爲KDE項目的Konqueror網頁瀏覽器所開發
  • Chakra(JScript9)
     Internet Explorer 瀏覽器
  • Chakra(JavaScript)html

    Microsoft Edge
  • Nashorn
    OpenJDK開源項目的一部分,用的是Oracle Java語言和工具組
  • JerryScript
    用於物聯網的輕量級引擎

在這些項目中,V8引擎因其在性能上的突出表現,倍受你們的關注,因此咱們也以介紹V8引擎爲主。V8是Google開源的高性能JavaScript引擎,用C++編寫。它用於谷歌瀏覽器,谷歌的開源瀏覽器,以及Node.js等等。java

速度是V8追求的主要設計目標之一,在一些性能測試中,V8比IE的JScript,Firefox中的SpiderMonkey以及Safari中的JavaScriptCore要快上數倍。相比其餘的JavaScript引擎轉化成字節碼或解釋執行,V8將其編譯成本地代碼,而且使用瞭如隱類型,內聯緩存等方法來提升性能。node

圖片描述

http://kourge.net/node/122linux

V8按照ECMA-262第5版中的規定實施ECMAScript,支持衆多操做系統,如windows、linux、android等,也支持其餘硬件架構,如IA32,X64,ARM等,具備很好的可移植和跨平臺特性。android

V8的工做過程

V8工做的整個過程與Java有些相似,大體分紅兩個階段:第一是編譯,第二是運行。與C++直接編譯成本地代碼不一樣的是,V8只有在函數調用時纔會編譯成本地代碼,這樣就提升了響應時間減小了時間開銷。git

圖片描述

圖片來源《WebKit技術內幕》github

在V8引擎中,源代碼先經過解析器轉變成抽象語法樹,這點同JavaScriptCore引擎同樣,不一樣於JavaScriptCore引擎,V8引擎中並不將抽象語法樹轉變成字節碼或者其餘中間表示,而是經過JIT全代碼生成器(full code generator)從抽象語法樹直接生成本地代碼,這樣作能夠減小抽象語法樹到字節碼的轉換時間,提升代碼的執行速度,但也是由於缺乏了轉換爲字節碼這一中間過程,也就減小了優化中間代碼的機會。web

下面來看一下V8引擎編譯JavaScript生成本地代碼使用了哪些主要類:正則表達式

  • Script類:表示是JavaScript代碼,既包含源代碼,又包含編譯以後生成的本地代碼,因此它既是編譯入口,又是運行入口
  • Compiler類:編譯器類,輔助Script類來編譯生成代碼,它主要起一個協調者的做用,會調用解釋器(Parser)來生成抽象語法樹和全代碼生成器,來爲抽象 語法樹生成本地代碼。
  • Parser類:將源代碼解釋並構建成抽象語法樹,使用AstNode類來建立它們,並使用Zone類來分配內存。
  • AstNode類:抽象語法樹節點類,是其餘全部節點的基類,它包含很是多的子類,後面會針對不一樣的子類生成不一樣的本地代碼。
  • AstVisitor類:抽象語法樹的訪問者類,主要用來遍歷抽象語法樹。
  • FullCodeGenerator:AstVisitor類的子類,經過遍歷抽象語法樹來爲JavaScrit生成本地代碼。

圖片描述

圖片來源《WebKit技術內幕》

JavaScript代碼編譯的過程大體爲:Script類調用Compiler類的Compile函數生成本地代碼。在該函數中,先使用Parser類來生成抽象語法樹;再使用FullCodeGenerator類來生成本地代碼。

圖片描述

圖片來源《WebKit技術內幕》

本地代碼與具體的硬件平臺密切相關,FullCodeGenerator使用多個後端來生成與平臺相匹配的本地彙編代碼。因爲FullCodeGenerator經過遍歷AST來爲每一個節點生成相應的彙編代碼,缺失了全局視圖,節點之間的優化也就無從談起。

JavaScript代碼編譯以前須要構建一個運行環境,因此JavaScript代碼編譯以前,V8引擎會構建衆多對象並加載一些內置的庫(如Math庫)。再次強調一下,在JavaScript源碼中,並不是全部的函數都被編譯生成本地代碼,而是延時編譯,在調用時纔會編譯。

因爲V8缺乏生成字節碼(中間表示)這一環節,缺乏必要的優化,爲了性能上的考慮,V8會在生成本地代碼後,使用數據分析器(Profiler)採集一些信息,而後根據這些信息對本地代碼進行優化,生成更高效率的本地代碼,這是一個逐步改進的過程。同時,當發現優化後的代碼性能並無提升甚至還有所下降時,V8將退回到原來的代碼。這些都是在運行階段用涉及到的技術。

如今咱們來看一下運行階段使用到的類:

  • Script: 表示是JavaScript代碼,既包含源代碼,又包含編譯以後生成的本地代碼,因此它既是編譯入口,又是運行入口
  • Execution: 運行代碼的輔助類,包含一些重要的函數,例如call,它輔助進入和執行Script中的本地代碼
  • JSFunction: 須要執行的JavaScript函數表示類
  • Runtime:運行這些本地代碼的輔助類,它的功能主要是提供運行時各類各樣的輔助函數,包括可是不限於屬性訪問、類型轉換、編譯、算數、位操做、比較、正則表達式等
  • Heap:運行本地代碼須要使用內存堆
  • MarkCompactCollector:垃圾回收機制的主要實現類,用來標記(Mark)、清除(Sweep)和整理(Compact)等基本的垃圾回收過程
  • SweeperThread:負責垃圾回收的線程

圖片描述
圖片來源《WebKit技術內幕》

首先,當某個JavaScript函數被調用時,使用編譯階段的類和操做編譯生成本地代碼。具體的工做方式是V8查找函數是否已經生成本地代碼,若是已經生成,那麼直接使用這個函數。不然,V8引擎會觸發生成本地代碼,這樣的工做方式能夠節約時間,減小去處理那些使用不到的代碼的時間。其次,執行編譯後的代碼爲JavaScript構建JS對象,這須要Runtime類來輔助建立對象,並須要從Heap類分配內容。再次,藉助Runtime類中的輔助函數來完成一些功能,如屬性訪問,類型轉換等。最後,將不用的空間進行標記清除和垃圾回收。

圖片描述
圖片來源《WebKit技術內幕》

V8特性簡介

一. 優化回滾

FullCodeGenerator編譯器基於抽象語法樹直接生成本地代碼,沒有中間表示層,因此不少時候沒有通過很好的優化。JavaScript引擎性能之爭很是激烈,沒有通過優化的代碼致使該引擎在性能上同有特別大的突破,而其餘引擎都在進度,有鑑於此,在2010年,V8引入了新的編譯器,這就是Crankshaft編譯器,它主要針對那些熱點函數進行優化。該編譯器基於JavaScript源代碼開始分析,而不是本地代碼,同時構建Hydtogen圖並基於此來進行優化分析。

FullCodeGenerator是一個簡單且快的編譯器,生成未優化的本地代碼,運行起來很慢;Crankshaft是一個相對慢的編譯器,生成高度優化的代碼。由FullCodeGenerator生成的未優化代碼Crankshaft優化代碼替換,傳送門

Crankshaft編譯器爲了性能考慮,一般會作出比較樂觀和大膽的預測,那就是編譯器認爲這些代碼比較穩定,變量類型不會發生改變,因此可以生成高效的本地代碼。可是在實際執行過程當中,由於JavaScript弱類型語言的特性,變量類型有可能會改變,在這種狀況下,V8會將該編譯器作的錯誤優化回滾到以前的通常狀況,這個過程稱爲優化回滾。

V8並不僅是第一次執行一個JavaScript函數時才編譯它;同一個JavaScript函數能夠被這些JIT編譯器屢次編譯。

基本流程是:

[JavaScript函數] ->
        第一次被調用 -> Full Code -> [初級編譯後的代碼]
         足夠熱以後 -> Crankshaft(Optimizing Compiler) -> [優化編譯後的代碼]
若是優化的代碼須要去優化(優化回滾) -> deoptimize -> 回到[初級編譯後的代碼]
    ... 周而復始 ...

示例以下:

var counter = 0;
function test(x,y){
    counter ++;
    if(counter < 10000000){
        // do something
        return 123;
    }
    var unknown = new Date();
    console.log(unknown);
}

函數test被調用屢次後,V8引擎可能會觸發Crankshaft編譯器來生成優化的代碼,優化的代碼認爲示例代碼的類型等信息都已經被獲知,但事實上還未真正執行到new Date()這個地方,並未獲取unknown這個變量的類型,V8只得將該部分的代碼進行回滾。優化回滾是一個很費時的操做,因此在寫代碼的過程當中,儘可能不要觸發這個過程。

二. 隱類型和內嵌緩存

咱們都知道JavaScript屬於動態類型語言,只有在運行時才能肯定變量的類型,在運行時計算和決定類型,會帶來嚴重的性能損失,這也就致使了JavaScript語言的運行效率比C++或Java都要低不少。

主要體如今如下幾個部分:

  1. 編譯肯定位置:
    C++在編譯階段對象的屬性和偏移信息都計算完成;而這些信息JavaScript只有在執行階段才能夠肯定
  2. 偏移信息共享:
    C++屬於靜態類型語言,不能在執行時動態改變類型,這些對象都是共享偏移信息的。訪問對象時就按編譯時的偏移量便可;JavaScript每一個對象都是自描述,屬性和位置偏移信息都包含在自身的結構中。

    一個簡單的C++函數:

    class Class1 {
         int x;
         int y;
     }
     int add(Class1 a, Class1 b){
         return a.x*a.y + b.x*b.y;
     }

    示例代碼中的類型和對象的結構表示,以下圖:
    clipboard.png
    圖片來源《WebKit技術內幕》

    一個簡單的JavaScript函數:

    function add(a,b){
        return a.x*a.y + b.x*b.y; // 這裏對象a和b的類型未知
    }
    var a = {x:3.3,y:5.5};
    var b = {x:4.4,y:6.6};

    示例代碼中對象a和b的結構表示,以下圖:
    clipboard.png
    圖片來源《WebKit技術內幕》

  3. 偏移信息查找:
    C++中查找偏移地址很簡單,都是在編譯代碼時,對使用到某類型的成員變量直接設置偏移量。而對於JavaScript,使用到一個對象則須要經過屬性名匹配才能查找到對應的值

由於對象屬性的訪問很是廣泛並且次數很是頻繁,像C++這種經過偏移量來訪問值使用少數兩個彙編指定就能完成,可是Javascript這種經過屬性名來匹配對於性能形成的影響可能會多不少倍,由於屬性名匹配須要特別長的時間,並且額外浪費不少內存空間。

有方法解決這一問題麼?答案是確定的。下面咱們就來看一下V8引擎是如何解決這一問題的。雖然JavaScript語言沒有類型的定義,可是V8使用類和偏移位置思想,將原本須要經過字符串匹配來查找屬性值的算法改進爲使用相似C++編譯器的偏移位置的機制來實現。這就是隱藏類(Hidden Class)

JavaScript對象的實如今V8中包含3個成員,第一個是隱藏類的指針,這是V8爲JavaScript對象建立的隱藏類。第二個指向這個對象包含的屬性值。第三個指向這個對象包含的元素。

clipboard.png
圖片來源《WebKit技術內幕》

隱藏類將對象劃分紅不一樣的組,對於相同的組,也就是該組內的對象擁有相同的屬性名和屬性值的狀況,將這些屬性名和對應的偏移位置保存在一個隱藏類中,組內的全部對象共享該信息。同時,也能夠識別屬性不一樣的對象。

V8引擎的發展歷史

  • 2008年9月,V8的第一個版本隨着Chrome的初版發佈。
  • 2010年12月,官方公佈V8的名爲Crankshaft的優化編譯器,與原來的Full Compiler一塊兒工做,聲稱較2008年版本提升50%性能。
  • 2015年7月7日,官方公佈又一個新的中爲TurBoFan的優化編譯器,主要提供ES6的新語法,以及提升性能。並代表該編譯器最終目標是所有替代Crankshaft編譯器。
  • 2015年7月17日,官方公佈集成了TurboFan的V8版本(v4.5)
  • 2015年8月28日,V8發佈v4.6版本
  • 2016年3月15日,V8發佈v5.0版本
  • 2016年7月18日,V8發佈v5.3版本,新增名爲Ignition的解析器(Interpreter),跟原有的優化編譯器(Crankshaft and TurboFan)進行串聯工做,提供了更加優化的內存使用方案,主要針對於低內存的Android設備,並稱在將來會普及到全平臺。
  • 2016年9月9日,V8發佈v5.4版本
  • 2016年10月24日,V8發佈v5.5版本,在5.5版本中開始支持ES7異步函數,這使得編寫使用和建立Promise的代碼變得更加容易。
  • 2016年12月2日,V8發佈v5.6版本,從5.6版本開始,V8能夠優化整個JavaScript語言。並且,許多語言功能都是經過V8中的新優化管道發送的。該管道使用V8的Ignition解釋器做爲基準,並使用V8更強大的TurboFan優化編譯器優化常常執行的方法。新的流水線激活了新的語言功能(例如ES2015和ES2016規範中的許多新功能)或Crankshaft(V8的「經典」優化編譯器)沒法優化某種方法(例如try-catch,with)的狀況。
  • 2017年2月6日,V8發佈v5.7版本
  • 2017年3月20日,V8發佈v5.8版本
  • 2017年4月27日,V8發佈v5.9版本,V8 5.9將成爲默認啓用Ignition + Turbofan的第一個版本。通常來講,這種交換機應該能夠下降內存消耗,而且能夠更快地啓動Web應用程序。
  • 2017年6月9日,V8發佈v6.0版本,V8 6.0引入了對SharedArrayBuffer的支持,SharedArrayBuffer是一種在JavaScript工做人員之間共享內存並在工做人員之間同步控制流的低級機制。 SharedArrayBuffers爲JavaScript提供了對共享內存,原子和futex的訪問。 SharedArrayBuffers還解鎖了經過asm.js或WebAssembly將線程化應用程序移植到Web的功能。
  • 2017年8月3日,V8發佈v6.1版本
  • 2017年9月11日,V8發佈v6.2版本
  • 2017年10月25日,V8發佈v6.3版本,改進了速度和內存消耗,詳細
  • 2017年12月19日,V8發佈v6.4版本,提高了速度和優化內存消耗,詳細
  • 2018年2月1日,V8發佈v6.5版本,編譯速度顯著提高,詳細
  • 2018年3月27日,V8發佈v6.6版本,異步性能大幅提高,詳細
相關文章
相關標籤/搜索