淺淡 JS Engine 機制

Xuan Li
8 min readOct 18, 2019

--

Javascript 引擎基本上拿到我們所撰寫 js file 時所經過的步驟為圖例,黃色背景的部分則是實際上 Javscript Code 運行的部分,首先經過 interpreter 轉譯為 bytecode 執行,大多數的 JS Engine 都會在經過編譯器處理並同時進行可優化的部分然後執行編譯完後的檔案,但避免編譯後的程式碼與原本的有落差或者錯誤,JS Engine 同樣會有防衛機制再將優化後的程式碼反轉回去,記著每個 JS 引擎實際上產出優化程式碼的步驟並不一定相同,下圖只是以 V8 為例

JS Engine flow

而本文都將以 V8 引擎機制為例

套入上圖的概念

  1. Interpreter — 負責編譯程式碼為 bytecode 並執行,在 V8 所使用的為 Ignition,而執行同時 Ignition 也會紀錄 profilling data 準備給 compiler 優化使用
  2. Optimizing compiler — 當 bytecode 執行時,若函式被呼叫次數頻繁的話, bytecode & profilling data 會被傳入到 compiler,V8 使用 TurboFan,基於 data 產出一個優化的程式碼

基本上每家的 JS Engine 處理過程都會有所差別,但共同的是都會有 parser, interpreter, compiler pipeline,interpreter 負責產生 bytecode ,但 bytecode 實際上實行並不是非常有效率的,所以再丟給 compiler pipe 去優化,雖然優化可能耗費稍微長一點的時間,但最後產出來的是更接近底層的 machine code ,相對也更有效率一些

引擎的組成主要是兩個元件

  1. Memory Heap
  2. Call Stack

大家常用的 Web API 好比 setTimOut, setInterval, document 這些並不包含在 JS 引擎裡面

而是由各家瀏覽器所提供

Javascript system

JS 是著名的單線程,也就是只有一次只能執行一件事情, Call Stack 有點像是記錄我們現在執行到哪一個程式的部分,好比我們進入了 a 函式就會在 call stack 堆一層 A 函式,讀到 return 或終止時則從 call stack 把 A 函式丟出來,如下圖

Call Stack

Stack 能容納的堆疊是有上限的,也因此要特別注意測試函式以免發生 Stack 炸裂的情況,例如無限迴圈或者 Recursive Function 的構成,雖然瀏覽器針對這種狀況有做防衛機制並會有 error 提醒,寫程式時還是得注意一下

Blowing call stack

單線程雖然簡易明瞭,但有一個嚴重的問題點就是當其中一個函式如果大道不可思議進而嚴重卡住大部分的執行時間,那在其之後的函式就得耗費大量時間等待,除此之外,因為 JS 是瀏覽器中是屬於 Render-blocking ,同時也可能影響到畫面產生的時間,這在使用者體驗上是致命的,針對此問題出來的解法便是 asynchronous callbacks,而 node.js 的實作上很大部分的原理就是透過 Event loop 解決單一線程的困擾,本文著重引擎的細節,姑且不討論此部分

How JS Engine handle object model

JS 是相對比較鬆散的語法,所有變數的 type, property, value 等等都可以在宣告後輕易做更動,甚至移除 property,這也造成在 JS 在抓取值的步驟上相對棘手,因為存放記憶體位址可能因為上述原因輕易被更動,再來是鬆散的架構下同一個物件的 property 也未必放在連續的記憶體位址上,因此在 access property 相對比較沒效率,如下圖儲存的資訊並沒記憶體位址

JS Engine 針對這部份提出了 Shape 的機制去加快尋找的方法,在建立 Object 的同時引擎也建立一個 Shape,不同於 Object,shape 紀錄了 property names 和其他 attributes,差別在於 shape並不紀錄 value 而是對 property 紀錄一個 offset

Object & shape infos

有了 offset 引擎便可以透過它更快找到 property 存放地方去抓取值,重要的是同一種 shape 會共享 shape chain,但特別特別注意,可以發現到 shape 的建立是根據 property order 的,假使今天c 物件為 { y: 1, x: 2 } 則會是不一樣的 shape 而不會與 a, b 共享 shape path,而是另外建立 shape

Share shape instance

當建立物件以及在物件上新增 property 時都會有新的 shape 產生,而每一次新增的 shape 都是相依的,也就是相依的 shape 會串成一條道路,如下圖

Shape chain

當今天以 o.x 找值時便會透過這條 chain 找到包含 x 的 shape,但假使 property 一多,shape 的長度就會非常驚人,而若每次都從最底層的 shape 向上尋找,那麼最壞狀況是 O(n) 呢,針對此 JS Engine 有實作 Shape Table 去做 shape mapping

Shape Table mapping

以上是有關 shape 的機制,shape 是此參考影片講者的用語,或許大家聽過 hidden class 其實就是相同的東西,而有了 shape 機制也同時讓 JS 引擎能夠做另一項優化 Inline Caching

Inline Caching 是引擎讓 JS 執行更快的主要關鍵之一,透過 shape 紀錄了 property 的位址,再透過 Inline caching 減少查詢位址的時間,主要的原理在於,當今天一個函式呼叫並回傳物件中的 property 時,第一次函式會沿著 shape chain 去查詢,透過 get_by_id 尋找 arg1也就是參數 o 並把找到的結果存到 loc0 ,此步驟的同時會把 inline cache 也放進函式,也就是下面的兩個 slot

inline cache

當函式再次被呼叫時,相比於第一次需要繁瑣的步驟去查詢 shape chain 時,此時 inline cache 已經存好上次呼叫的 offset 以及 shape,因此接下來當呼叫此函式時,若物件共享的是同一個 shape 則可以省略 look up 的繁瑣步驟,而直接從 offset 得到對應 property 撈取值,呼叫越多次就省越多次,當然假使儲存的 shape 不同的話還是得再從頭跑過一次

Inline Caching

上述就是引擎背後的處理小細節,當然這只是引擎中的一小部分,如果各位有看過其他類似的講解或者直得推薦的文章也歡迎留言互相學習,而文章主要是看完參考影片以及文章所做的小筆記,圖片的部分也都是擷取影片中的內容,大家也可以嘗試一下畫畫看或許更幫助理解

如果覺得這篇文章多多少少有幫助的話再請幫我點個 clap ,若有錯誤也麻煩各位在底下留言,深怕不小心讓讀者讀到錯誤的觀念,謝謝

Reference -

--

--

Xuan Li
Walk Out 技術共筆

1. 30歲的前端工程師 2. 身體脂肪抗爭中的運動愛好者 - 健身, 慢跑, 游泳 3. 喜歡閱讀 -> 近期閱讀 自私的基因 4. 喜歡看劇看電影並從中獲得生活啟發 5. 嘗試練習分享中