JavaScript深刻淺出第4課:V8引擎是如何工做的?

摘要: 性能彪悍的V8引擎。javascript

JavaScript深刻淺出》系列java

最近,JavaScript生態系統又多了2個很是硬核的項目。node

大神Fabrice Bellard發佈了一個新的JS引擎QuickJS,能夠將JavaScript源碼轉換爲C語言代碼,而後再使用系統編譯器(gcc或者clang)生成可執行文件。git

Facebook爲React Native開發了新的JS引擎Hermes,用於優化安卓端的性能。它能夠在構建APP的時候將JavaScript源碼編譯爲Bytecode,從而減小APK大小、減小內存使用,提升APP啓動速度。程序員

做爲JavaScript程序員,只有極少數人有機會和能力去實現一個JS引擎,可是理解JS引擎仍是頗有必要的。本文將介紹一下V8引擎的原理,但願能夠給你們一些幫助。github

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小程序

這個N階乘的彙編代碼是我大學時期寫的,已是N年前的事情了,它須要處理10進制與2進制的轉換,須要使用多個字節保存大整數,最多能夠計算大概500左右的N階乘。後端

還有一點,不一樣類型的CPU的指令集是不同的,那就意味着得給每一種CPU重寫彙編代碼,這就很崩潰了。。。

還好,JavaScirpt引擎能夠將JS代碼編譯爲不一樣CPU(Intel, ARM以及MIPS等)對應的彙編代碼,這樣咱們纔不要去翻閱每一個CPU的指令集手冊。固然,JavaScript引擎的工做也不僅是編譯代碼,它還要負責執行代碼、分配內存以及垃圾回收

雖然瀏覽器很是多,可是主流的JavaScirpt引擎其實不多,畢竟開發一個JavaScript引擎是一件很是複雜的事情。比較出名的JS引擎有這些:

還有,最近發佈QuickJSHermes也是JS引擎,它們都超越了瀏覽器範疇,Atwood's Law再次獲得了證實:

Any application that can be written in JavaScript, will eventually be written in JavaScript.

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在工業界已經很是成功了,同時它還得到了學術界的確定,拿到了ACM SIGPLAN的Programming Languages Software Award

V8's success is in large part due to the efficient machine code it generates. Because JavaScript is a highly dynamic object-oriented language, many experts believed that this level of performance could not be achieved. V8's performance breakthrough has had a major impact on the adoption of JavaScript, which is nowadays used on the browser, the server, and probably tomorrow on the small devices of the internet-of-things.

JavaScript是一門動態類型語言,這會給編譯器增長很大難度,所以專家們以爲它的性能很難提升,可是V8竟然作到了,生成了很是高效的machine code(實際上是彙編代碼),這使得JS能夠應用在各個領域,好比Web、APP、桌面端、服務端以及IOT。

嚴格來說,V8所生成的代碼是彙編代碼而非機器代碼,可是V8相關的文檔、博客以及其餘資料都把V8生成的代碼稱做machine code。彙編代碼與機器代碼不少是一一對應的,也很容易互相轉換,這也是反編譯的原理,所以他們把V8生成的代碼稱爲Machine Code也何嘗不可,可是並不嚴謹。

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源碼編譯爲彙編代碼,其流程圖以下:

簡單地說,Parser將JS源碼轉換爲AST,而後Ignition將AST轉換爲Bytecode,最後TurboFan將Bytecode轉換爲通過優化的Machine Code(其實是彙編代碼)。

  • 若是函數沒有被調用,則V8不會去編譯它。
  • 若是函數只被調用1次,則Ignition將其編譯Bytecode就直接解釋執行了。TurboFan不會進行優化編譯,由於它須要Ignition收集函數執行時的類型信息。這就要求函數至少須要執行1次,TurboFan纔有可能進行優化編譯。
  • 若是函數被調用屢次,則它有可能會被識別爲熱點函數,且Ignition收集的類型信息證實能夠進行優化編譯的話,這時TurboFan則會將Bytecode編譯爲Optimized Machine Code,以提升代碼的執行性能。

圖片中的紅線是逆向的,這的確有點奇怪,Optimized Machine Code會被還原爲Bytecode,這個過程叫作Deoptimization。這是由於Ignition收集的信息多是錯誤的,好比add函數的參數以前是整數,後來又變成了字符串。生成的Optimized Machine Code已經假定add函數的參數是整數,那固然是錯誤的,因而須要進行Deoptimization。

function add(x, y) {
    return x + y;
}

add(1, 2);
add("1", "2");

在運行C、C++以及Java等程序以前,須要進行編譯,不能直接執行源碼;但對於JavaScript來講,咱們能夠直接執行源碼(好比:node server.js),它是在運行的時候先編譯再執行,這種方式被稱爲即時編譯(Just-in-time compilation),簡稱爲JIT。所以,V8也屬於JIT編譯器。

Ignition:解釋器

Node.js是基於V8引擎實現的,所以node命令提供了不少V8引擎的選項,使用node的--print-bytecode選項,能夠打印出Ignition生成的Bytecode。

factorial.js以下,因爲V8不會編譯沒有被調用的函數,所以須要在最後一行調用factorial函數。

function factorial(N) {
    if (N === 1) {
        return 1;
    } else {
        return N * factorial(N - 1);
    }
}

factorial(10); // V8不會編譯沒有被調用的函數,所以這一行不能省略

使用node命令(node版本爲12.6.0)的--print-bytecode選項,打印出Ignition生成的Bytecode:

node --print-bytecode factorial.js

控制檯輸出的內容很是多,最後一部分是factorial函數的Bytecode:

[generated bytecode for function: factorial]
Parameter count 2
Register count 3
Frame size 24
   18 E> 0x3541c2da112e @    0 : a5                StackCheck
   28 S> 0x3541c2da112f @    1 : 0c 01             LdaSmi [1]
   34 E> 0x3541c2da1131 @    3 : 68 02 00          TestEqualStrict a0, [0]
         0x3541c2da1134 @    6 : 99 05             JumpIfFalse [5] (0x3541c2da1139 @ 11)
   51 S> 0x3541c2da1136 @    8 : 0c 01             LdaSmi [1]
   60 S> 0x3541c2da1138 @   10 : a9                Return
   82 S> 0x3541c2da1139 @   11 : 1b 04             LdaImmutableCurrentContextSlot [4]
         0x3541c2da113b @   13 : 26 fa             Star r1
         0x3541c2da113d @   15 : 25 02             Ldar a0
  105 E> 0x3541c2da113f @   17 : 41 01 02          SubSmi [1], [2]
         0x3541c2da1142 @   20 : 26 f9             Star r2
   93 E> 0x3541c2da1144 @   22 : 5d fa f9 03       CallUndefinedReceiver1 r1, r2, [3]
   91 E> 0x3541c2da1148 @   26 : 36 02 01          Mul a0, [1]
  110 S> 0x3541c2da114b @   29 : a9                Return
Constant pool (size = 0)
Handler Table (size = 0)

生成的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:編譯器

使用node命令的--print-code以及--print-opt-code選項,打印出TurboFan生成的彙編代碼:

node --print-code --print-opt-code factorial.js

我是在Mac上運行的,結果以下圖所示:

比起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引擎優化編譯的彙編代碼,固然這一點還須要測試數據來證實。

Orinoco:垃圾回收

強大的垃圾回收功能是V8實現提升性能的關鍵之一,由於它能夠在避免影響JS代碼執行的狀況下,同時回收內存空間,提升內存利用效率。

關於垃圾回收,我在JavaScript深刻淺出第3課:什麼是垃圾回收算法?中有詳細介紹,這裏就再也不贅述了。

JS引擎的將來

V8引擎確實很強大,可是它也不是無所不能的,簡單地分析均可以發現一些能夠優化的點。

我有一個新的想法,還沒想好名字,不妨稱做Optimized TypeScript Engine:

  • 使用TypeScript編程,遵循嚴格的類型化編程規則,不要寫成AnyScript了;
  • 構建的時候將TypeScript直接編譯爲Bytecode,而不是生成JS文件,這樣運行的時候就省去了Parse以及生成Bytecode的過程;
  • 運行的時候,須要先將Bytecode編譯爲對應CPU的彙編代碼;
  • 因爲採用了類型化的編程方式,有利於編譯器優化所生成的彙編代碼,省去了不少額外的操做;

這個想法其實能夠基於V8引擎來實現,技術上應該是可行的:

  • 將Parser以及Ignition拆分出來,用於構建階段;
  • 刪掉TurboFan處理JS動態特性的相關代碼;

這樣作,能夠將JS引擎簡化不少,一方面再也不須要parse以及生成bytecode,另外一方面編譯器再也不須要由於JavaScript動態特性作不少額外的工做。所以能夠減小CPU、內存以及電量的使用,優化性能,惟一的問題多是必須使用嚴格的TS語法進行編程。

爲啥要這樣作呢?由於對於IOT硬件來講,CPU、內存、電量都是須要省着點用的,不是每個智能家電都須要裝一個驍龍855,若是但願把JS應用到IOT領域,必然須要從JS引擎角度去進行優化,只是去作上層的框架是沒有用的。

其實,Facebook的Hermes差很少就是這麼幹的,只是它沒有要求用TS編程。

這應該是JS引擎的將來,你們會看到愈來愈多這樣的趨勢。

關於JS,我打算花1年時間寫一個系列的博客**《JavaScript深刻淺出》**,你們還有啥不太清楚的地方?不妨留言一下,我能夠研究一下,而後再與你們分享一下。歡迎添加個人我的微信(KiwenLau),我是Fundebug的技術負責人,一個對JS又愛又恨的程序員。

參考

關於Fundebug

Fundebug專一於JavaScript、微信小程序、微信小遊戲、支付寶小程序、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有陽光保險、核桃編程、荔枝FM、掌門1對一、微脈、青團社等衆多品牌企業。歡迎你們免費試用!

img

版權聲明

轉載時請註明做者 Fundebug以及本文地址: https://blog.fundebug.com/2019/07/16/how-does-v8-work/

相關文章
相關標籤/搜索