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

来源:https://www.cnblogs.com/fundebug/archive/2019/07/16/how-does-v8-work.html
-Advertisement-
Play Games

摘要: 性能彪悍的V8引擎。 《 "JavaScript深入淺出" 》系列 : "JavaScript深入淺出第1課:箭頭函數中的this究竟是什麼鬼?" "JavaScript深入淺出第2課:函數是一等公民是什麼意思呢?" "JavaScript深入淺出第3課:什麼是垃圾回收演算法?" "JavaS ...


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

JavaScript深入淺出》系列

最近,JavaScript生態系統又多了2個非常硬核的項目。

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

Facebook為React Native開發了新的JS引擎Hermes,用於優化安卓端的性能。它可以在構建APP的時候將JavaScript源碼編譯為Bytecode,從而減少APK大小、減少記憶體使用,提高APP啟動速度。

作為JavaScript程式員,只有極少數人有機會和能力去實現一個JS引擎,但是理解JS引擎還是很有必要的。本文將介紹一下V8引擎的原理,希望可以給大家一些幫助。

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對1、微脈、青團社等眾多品牌企業。歡迎大家免費試用!

img

版權聲明

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 當時遇見這個問題 是醫院手麻系統大批量數據展示,由於是舊項目系統沒有使用到前端的架構 只能使用JQ,JS, css完成 也謝謝給予我支持的同行們 固定首行數據: 採用函數的方式進行 JQ /** * 功能:固定表頭 * 參數 viewid 表格的id * scrollid 滾動條所在容器的id * ...
  • 一、項目介紹 運用angular+angular-cli+angular-router+ngrx/store+rxjs+webpack+node+wcPop等技術實現開發的仿微信angular版聊天室angular-chatroom實例項目,實現了下拉刷新、聊天消息右鍵菜單、發送消息、表情(動圖), ...
  • javascript中用&&跟||來簡化if{}else{}的寫法 [toc] 1. if else的寫法 表示如果傳入數組為非空,則賦值傳入的經緯度數據;如果表示如果傳入數組為空,則賦值固定的經緯度數據。 2. javascript中 && ||的用法 a() && b();如果執行a()後返回t ...
  • JS補充 document也是windows的一個子對象 a標簽點擊事件 要想設置點擊a標簽,執行某種方法,推薦在a標簽的herf屬性使用JavaScript偽協議,實現點擊之後執行的js方法,而不是設置click 例如: windows對象對話框 windows自帶的幾個彈出對話框方法 可輸入內容 ...
  • "HTML基本結構" "HTML頭部標簽" meta 標簽 link 標簽 base 標簽 "HTML標簽" "HTML 標簽分類" "HTML 標簽屬性" "排版標簽" "文本格式化標簽" "圖片標簽 img" "鏈接標簽 a" "列表標簽" "表格標簽 table" "表單標簽 form" in ...
  • 在家裡安裝這個包,總是報錯安裝失敗,換成最快的淘寶鏡像也是如此,先卸載重新安裝亦是如此,於是想到了原因,到了公司,公司的網是可以連接國外的,安裝成功了! 也就是說,需要翻牆才可以裝成功。 ...
  • 近日,需要在網頁上做一個純H5的、基於三維場景的應用,網上搜了下,發現ThingJS平臺(https://www.thingjs.com/)是個不錯的選擇...... ThingJS 是一個物聯網3D可視化PaaS平臺,基於WebGL,相容各種瀏覽器及移動設備,零門檻、高效率、低成本開發各類3D應用 ...
  • 閉包的形成與變數的作用域及生命周期密切相關,所以在理解閉包前,須理解變數作用域。作用域分全局和局部作用域,是指變數有效訪問的範圍。變數無權訪問子作用域,只能訪問自己和父級以上的作用域。 預編譯 當函數執行時,會創建一個執行期上下文(即作用域)的對象AO(存儲在 中), 一個新的AO指向 定義了一個函 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...