Day00:V8 bytecode 系列文介紹


自我介紹

相關背景請參考個人部落格中的關於我頁面,參賽動機下面會寫到,目前規劃這個系列文會有八篇:

  1. Day00:V8 bytecode 系列文介紹(基本知識、產生 bytecode)
  2. Day01:從變數看 bytecode
  3. Day02:從判斷式看 bytecode
  4. Day03:從迴圈看 bytecode
  5. Day04:從函式看 bytecode
  6. Day05:從 class 看 bytecode
  7. Day06:從經典案例看 bytecode
  8. Day07:V8 bytecode 系列文總結

前言

我理解 bytecode 的啟蒙是這一篇文章:Understanding V8’s Bytecode,中譯版:理解 V8 的字節碼,這篇文章裡面有提到 V8 在解析以及運行程式碼時的流程,大致上是這樣的:

  1. 解析 JavaScript 程式碼,產生 AST(Abstract Syntax Tree)
  2. 透過 Ignition 產生 bytecode
  3. 透過 TurboFan 從 bytecode 產生 machine code

上文中的這張圖片,很清楚地說明了 bytecode 的定位:

那會看 bytecode 有什麼好處呢?你可以更接近底層一點,知道 V8 如何去處理某一段程式碼,有時候你會發現,跟你想像中的不太一樣,這就是看 bytecode 的好玩之處!

舉例來說,我以前寫過的三篇文章都有看 bytecode 的段落,透過 bytecode 去看一些底層的東西:

  1. 我知道你懂 hoisting,可是你了解到多深?
  2. 所有的函式都是閉包:談 JS 中的作用域與 Closure
  3. 從 V8 bytecode 探討 let 與 var 的效能問題

在看這些 bytecode 時有一個問題,那就是參考資料太少,中文的不用說,連英文的都不多,而且有些文章並不是那麼好懂,深度有點太深。因此,才有了這個系列文的想法,希望能以一些簡單的範例帶著跟我一樣對 bytecode 有興趣的人來入門,然後來看懂 bytecode。

產生 bytecode

想要看 bytecode,第一步就是要先產生 bytecode。

產生 bytecode 的方法很簡單,透過 Node.js 搭配 --print-bytecode 這個 flag 就行了。但要注意的是 Node.js 內建的東西不少,所以會產生一大堆的 bytecode,這邊推薦的方法是把你想看的程式碼用一個特別的函式名稱包住,搭配 --print-bytecode-filter 來篩選函式名稱,就會容易很多,例如說以下程式碼:

function find_me_test() {
  var a = 1
  console.log(a)
}

find_me_test() //  V8 很智慧,記得要呼叫 function,否則不會產生程式碼

存檔成a.js,然後執行:node --print-bytecode --print-bytecode-filter="find_me*" a.js > byte_code.txt,接著打開byte_code.txt,就可以看到產生的內容:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
   83 E> 0x26e2ec856652 @    0 : a0                StackCheck 
  301 S> 0x26e2ec856653 @    1 : 0c 01             LdaSmi [1]
         0x26e2ec856655 @    3 : 26 fb             Star r0
  305 S> 0x26e2ec856657 @    5 : 13 00 00          LdaGlobal [0], [0]
         0x26e2ec85665a @    8 : 26 f9             Star r2
  313 E> 0x26e2ec85665c @   10 : 28 f9 01 02       LdaNamedProperty r2, [1], [2]
         0x26e2ec856660 @   14 : 26 fa             Star r1
  313 E> 0x26e2ec856662 @   16 : 57 fa f9 fb 04    CallProperty1 r1, r2, r0, [4]
         0x26e2ec856667 @   21 : 0d                LdaUndefined 
  320 S> 0x26e2ec856668 @   22 : a4                Return 
Constant pool (size = 2)
Handler Table (size = 0)
1

以上程式碼其實沒有那麼好懂,那是因為少了一個關鍵的資訊:constant pool,有一些常數會放到這個裡面,例如說 console.logconsolelog 你會發現在程式碼裡面都沒有出現,因為這兩個關鍵字都在 constant pool 裡面。

在一般的 Node.js 裡面,你只會看到:Constant pool (size = 2),卻看不到裡面的內容。

想要看到裡面的內容的話,必須要自己 build debug 版本的 Node.js,或是直接去 build debug 版本的 V8,因為我想說反正也沒編譯過 V8,就決定來編譯看看了。

(參考資料:Node.js Bytecode Constant Pool Output

編譯 V8

想要編譯 V8,官方文件提供了滿完整的說明,以下是一些相關文件:

  1. Checking out the V8 source code
  2. depot_tools

但基本上步驟滿簡單的,我的作業系統是 macOS Mojave 10.14.4,步驟如下:

  1. 安裝 depot_tools,步驟如下
  2. 下載 depot_tools:git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
  3. 設置環境變數:export PATH=$PATH:/path/to/depot_tools
  4. 下載 v8:fetch v8
  5. 接著我在編譯時碰到問題,猜測是需要新版的作業系統,因此我編譯的是比較舊版的 V8:git checkout -b 7.3 -t branch-heads/7.3
  6. 安裝必要套件:gclient sync
  7. build debug version:tools/dev/gm.py x64.debug

編譯跟下載套件都需要一段時間,需要耐心等待。編譯完成以後,就可以在 out/x64.debug 底下找到一個執行檔 d8,以後都用這個 d8 即可。

我們用同樣的指令,只是執行檔改成 d8,去編譯上面那一段程式碼看看:./d8 --print-bytecode --print-bytecode-filter="find_me*" a.js > byte_code.txt,結果為:

[generated bytecode for function: find_me_test]
Parameter count 1
Frame size 24
   21 E> 0x10715a29dcaa @    0 : a5                StackCheck 
  239 S> 0x10715a29dcab @    1 : 0c 01             LdaSmi [1]
         0x10715a29dcad @    3 : 26 fb             Star r0
  243 S> 0x10715a29dcaf @    5 : 13 00 00          LdaGlobal [0], [0]
         0x10715a29dcb2 @    8 : 26 f9             Star r2
  251 E> 0x10715a29dcb4 @   10 : 28 f9 01 02       LdaNamedProperty r2, [1], [2]
         0x10715a29dcb8 @   14 : 26 fa             Star r1
  251 E> 0x10715a29dcba @   16 : 59 fa f9 fb 04    CallProperty1 r1, r2, r0, [4]
         0x10715a29dcbf @   21 : 0d                LdaUndefined 
  258 S> 0x10715a29dcc0 @   22 : a9                Return 
Constant pool (size = 2)
0x10715a29dc31: [FixedArray] in OldSpace
 - map: 0x1071768807b1 <Map>
 - length: 2
           0: 0x1071ec1100e9 <String[#7]: console>
           1: 0x1071ec10fbe9 <String[#3]: log>
Handler Table (size = 0)
1

會發現 constant pool 的內容顯示出來了。至此,前置作業已經都準備完成。

結語

在這篇文章中簡單介紹了 bytecode 的定位以及系列文未來的方向,在之後的文章裡面會慢慢補齊一些基礎知識,也會從各個基礎的語法來學習 bytecode。

#javascript
V8 在處理 JavaScript 的程式碼時,會先將程式碼轉換成中間碼(bytecode)才執行。因此,藉由觀察 V8 bytecode,可以更理解一段 JavaScript 程式碼在 V8 眼裡是什麼樣子。這個系列文會以一系列的簡單程式碼為例,帶大家一起研究 bytecode






Related Posts

Day 6 - 使用 Tenor API 讓惠惠傳送隨機爆炸 gif

Day 6 - 使用 Tenor API 讓惠惠傳送隨機爆炸 gif

[Day 05] 走訪器模式,建造者模式,責任鏈模式,解譯器模式

[Day 05] 走訪器模式,建造者模式,責任鏈模式,解譯器模式

[ 紀錄 ] 實戰練習 - Todo List ( 以 JS 實作前端 + PHP 後端 )

[ 紀錄 ] 實戰練習 - Todo List ( 以 JS 實作前端 + PHP 後端 )



Comments