摘要:概述的解釋器優化器代碼可能在字節碼或者優化后的機器碼狀態下執行,而生成字節碼速度很快,而生成機器碼就要慢一些了。比如有一個函數,從獲取值引擎生成的字節碼結構是這樣的指令是獲取參數指向的對象,并存儲在,第二步則返回。
1 引言
本期精讀的文章是:JS 引擎基礎之 Shapes and Inline Caches
一起了解下 JS 引擎是如何運作的吧!
JS 的運作機制可以分為 AST 分析、引擎執行兩個步驟:
JS 源碼通過 parser(分析器)轉化為 AST(抽象語法樹),再經過 interperter(解釋器)解析為 bytecode(字節碼)。
為了提高運行效率,optimizing compiler(優化編輯器)負責生成 optimized code(優化后的機器碼)。
本文主要從 AST 之后說起。
2 概述 JS 的解釋器、優化器JS 代碼可能在字節碼或者優化后的機器碼狀態下執行,而生成字節碼速度很快,而生成機器碼就要慢一些了。
V8 也類似,V8 將 interpreter 稱為 Ignition(點火器),將 optimizing compiler 成為 TurboFan(渦輪風扇發動機)。
可以理解為將代碼先點火啟動后,逐漸進入渦輪發動機提速。
代碼先快速解析成可執行的字節碼,在執行過程中,利用執行中獲取的數據(比如執行頻率),將一些頻率高的方法,通過優化編譯器生成機器碼以提速。
火狐使用的 Mozilla 引擎有一點點不同,使用了兩個優化編譯器,先將字節碼優化為部分機器碼,再根據這個部分優化后的代碼運行時拿到的數據進行最終優化,生成高度優化的機器碼,如果優化失敗將會回退到部分優化的機器碼。
筆者:不同前端引擎對 JS 優化方式大同小異,后面會繼續列舉不同前端引擎在解析器、編譯器部分優化的方式。
微軟的 Edge 瀏覽器,使用的 Chakra 引擎,優化方式與 Mozilla 很像,區別是第二個最終優化的編譯器同時接收字節碼和部分優化的機器碼產生的數據,并且在優化失敗后回退到第一步字節碼而不是第二步。
Safari、React Native 使用的 JSC 引擎則更為極端,使用了三個優化編譯器,其優化是一步步漸進的,優化失敗后都會回退到第一步部分優化的機器碼。
為什么不同前端引擎會使用不同的優化策略呢?這是由于 JS 要么使用解釋器快速執行(生成字節碼),或者優化成機器碼后再執行,但優化消耗時間的并不總是小于字節碼低效運行損耗的時間,所以有些引擎選擇了多個優化編譯器,逐層優化,盡可能在解析時間與執行效率中找到一個平衡點。
JS 的對象模型JS 是基于面向對象的,那么 JS 引擎是如何實現 JS 對象模型的呢?他們用了哪些技巧加速訪問 JS 對象的屬性?
和解析器、優化器一樣,大部分主流 JS 引擎在對象模型實現上也很類似。
ECMAScript 規范確定了對象模型就是一個以字符串為 key 的字典,除了其值以外,還定義了 Writeable Enumerable Configurable 這些配置,表示這個 key 能否被重寫、遍歷訪問、配置。
雖然規范定義了 [[]] 雙括號的寫法,那這不會暴露給用戶,暴露給用戶的是 Object.getOwnPropertyDescriptor 這個 API,可以拿到某個屬性的配置。
在 JS 中,數組是對象的特殊場景,相比對象,數組擁有特定的下標,根據 ECMAScript 規范規定,數組下標的長度最大為 232?1。同時數組擁有 length 屬性:
length 只是一個不可枚舉、不可配置的屬性,并且在數組賦值時,會自動更新數值:
所以數組是特殊的對象,結構完全一致。
屬性訪問效率優化屬性訪問是最常見的,所以 JS 引擎必須對屬性訪問做優化。
ShapesJS 編程中,給不同對象相同的 key 名很常見,訪問不同對象的同一個 propertyKey 也很常見:
const object1 = { x: 1, y: 2 }; const object2 = { x: 3, y: 4 }; function logX(object) { console.log(object.x); // ^^^^^^^^ } logX(object1); logX(object2);
這時 object1 與 object2 擁有一個相同的 shape。拿擁有 x、y 屬性的對象來看:
如果訪問 object.y,JS 引擎會先找到 key y,再查找 [[value]]。
如果將屬性值也存儲在 JSObject 中,像 object1 object2 就會出現許多冗余數據,因此引擎多帶帶存儲 Shape,與真實對象隔離:
這樣具有相同結構的對象可以共享 Shape。所有 JS 引擎都是用這種方式優化對象,但并不都稱為 Shape,這里就不詳細羅列了,可以去原文查看在各引擎中 Shape 的別名。
Transition chains 和 Transition trees如果給一個對象增加了 key,JS 引擎如何生成新的 Shape 呢?
這種 Shape 鏈式創建的過程,稱為 Transition chains:
開始創建空對象時,JSObject 和 Shape 都是空,當為 x 賦值 5 時,在 JSObject 下標 0 的位置添加了 5,并且 Shape 指向了擁有字段 x 的 Shape(x),當賦值 y 為 6 時,在 JSObject 下標 1 的位置添加了 6,并將 Shape 指向了擁有字段 x 和 y 的 Shape(x, y)。
而且可以再優化,Shape(x, y) 由于被 Shape(x) 指向,所以可以省略 x 這個屬性:
筆者:當然這里說的主要是優化技巧,我們可以看出來,JS 引擎在做架構設計時沒有考慮優化問題,而在架構設計完后,再回過頭對時間和空間進行優化,這是架構設計的通用思路。
如果沒有連續的父 Shape,比如分別創建兩個對象:
const object1 = {}; object1.x = 5; const object2 = {}; object2.y = 6;
這時要通過 Transition trees 來優化:
可以看到,兩個 Shape(x) Shape(y) 別分繼承 Shape(empty)。當然也不是任何時候都會創建空 Shape,比如下面的情況:
const object1 = {}; object1.x = 5; const object2 = { x: 6 };
生成的 Shape 如下圖所示:
可以看到,由于 object2 并不是從空對象開始的,所以并不會從 Shape(empty) 開始繼承。
Inline Caches大概可以翻譯為“局部緩存”,JS 引擎為了提高對象查找效率,需要在局部做高效緩存。
比如有一個函數 getX,從 o.x 獲取值:
function getX(o) { return o.x; }
JSC 引擎 生成的字節碼結構是這樣的:
get_by_id 指令是獲取 arg1 參數指向的對象 x,并存儲在 loc0,第二步則返回 loc0。
當執行函數 getX({ x: "a" }) 時,引擎會在 get_by_id 指令中緩存這個對象的 Shape:
這個對象的 Shape 記錄了自己擁有的字段 x 以及其對應的下標 offset:
執行 get_by_id 時,引擎從 Shape 查找下標,找到 x,這就是 o.x 的查找過程。但一旦找到,引擎就會將 Shape 保存的 offset 緩存起來,下次開始直接跳過 Shape 這一步:
以后訪問 o.x 時,只要 Shape 相同,引擎直接從 get_by_id 指令中緩存的下標中可以直接命中要查找的值,而這個緩存在指令中的下標就是 Inline Cache.
數組存儲優化和對象一樣,數組的存儲也可以被優化,而由于數組的特殊性,不需要為每一項數據做完整的配置。
比如這個數組:
const array = ["#jsconfeu"];
JS 引擎同樣通過 Shape 與數據分離的方式存儲:
JS 引擎將數組的值多帶帶存儲在 Elements 結構中,而且它們通常都是可讀可配置可枚舉的,所以并不會像對象一樣,為每個元素做配置。
但如果是這種例子:
// 永遠不要這么做 const array = Object.defineProperty([], "0", { value: "Oh noes!!1", writable: false, enumerable: false, configurable: false });
JS 引擎會存儲一個 Dictionary Elements 類型,為每個數組元素做配置:
這樣數組的優化就沒有用了,后續的賦值都會基于這種比較浪費空間的 Dictionary Elements 結構。所以永遠不要用 Object.defineProperty 操作數組。
通過對 JS 引擎原理的認識,作者總結了下面兩點代碼中的注意事項:
盡量以相同方式初始化對象,因為這樣會生成較少的 Shapes。
不要混淆對象的 propertyKey 與數組的下標,雖然都是用類似的結構存儲,但 JS 引擎對數組下標做了額外優化。
3 精讀這次原理系列解讀是針對 JS 引擎執行優化這個點的,而網頁渲染流程大致如下:
可以看到 Script 在整個網頁解析鏈路中位置是比較靠前的,JS 解析效率會直接影響網頁的渲染,所以 JS 引擎通過解釋器(parser)和優化器(optimizing compiler)盡可能 對 JS 代碼提效。
Shapes需要特別說明的是,Shapes 并不是 原型鏈,原型鏈是面向開發者的概念,而 Shapes 是面向 JS 引擎的概念。
比如如下代碼:
const a = {}; const b = {}; const c = {};
顯然對象 a b c 之間是沒有關聯的,但共享一個 Shapes。
另外理解引擎的概念有助于我們站在語法層面對立面的角度思考問題:在 JS 學習階段,我們會執著于思考如下幾種創建對象方式的異同:
const a = {}; const b = new Object(); const c = new f1(); const d = Object.create(null);
比如上面四種情況,我們要理解在什么情況下,用何種方式創建對象性能最優。
但站在 JS 引擎優化角度去考慮,JS 引擎更希望我們都通過 const a = {} 這種看似最沒有難度的方式創建對象,因為可以共享 Shape。而與其他方式混合使用,可能在邏輯上做到了優化,但阻礙了 JS 引擎做自動優化,可能會得不償失。
Inline Caches對象級別的優化已經很極致了,工程代碼中也沒有機會幫助 JS 引擎做得更好,值得注意的是不要對數組使用 Object 對象下的方法,尤其是 defineProperty,因為這會讓 JS 引擎在存儲數組元素時,使用 Dictionary Elements 結構替代 Elements,而 Elements 結構是共享 PropertyDescriptor 的。
但也有難以避免的情況,比如使用 Object.defineProperty 監聽數組變化時,就不得不破壞 JS 引擎渲染了。
筆者寫 dob 的時候,使用 proxy 監聽數組變化,這并不會改變 Elements 的結構,所以這也從另一個側面證明了使用 proxy 監聽對象變化比 Object.defineProperty 更優,因為 Object.defineProperty 會破壞 JS 引擎對數組做的優化。
4 總結本文主要介紹了 JS 引擎兩個概念: Shapes 與 Inline Caches,通過認識 JS 引擎的優化方式,在編程中需要注意以下兩件事:
盡量以相同方式初始化對象,因為這樣會生成較少的 Shapes。
不要混淆對象的 propertyKey 與數組的下標,雖然都是用類似的結構存儲,但 JS 引擎對數組下標做了額外優化。
5 更多討論討論地址是:精讀《JS 引擎基礎之 Shapes and Inline Caches》 · Issue #91 · dt-fe/weekly
如果你想參與討論,請點擊這里,每周都有新的主題,周末或周一發布。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/95661.html
摘要:引言本周精讀的文章是,看看作者是如何解釋這個多態性含義的。讀完文章才發現,文章標題改為的多態性更妥當,因為整篇文章都在說,而使用場景不局限于。更多討論討論地址是精讀的多態性如果你想參與討論,請點擊這里,每周都有新的主題,周末或周一發布。 1 引言 本周精讀的文章是:surprising-polymorphism-in-react-applications,看看作者是如何解釋這個多態性含...
摘要:引言這個是針對的。一般結合使用,因為請求級別的緩存與具有頁面攔截功能的最配。本周精讀的文章是,介紹了瀏覽器緩存接口的基本語法。包含任意命名空間,可以通過創建或訪問。精讀筆者利用實現了純瀏覽器端的后端渲染。前端精讀幫你篩選靠譜的內容。 1 引言 caches 這個 API 是針對 Request Response 的。caches 一般結合 Service Worker 使用,因為請求級...
摘要:用解釋虛擬機內聯緩存本文轉載自眾成翻譯譯者鏈接原文我知道如何實現用語言或者語言的子集來實現運行該語言虛擬機。有時候我們用了錯誤的抽象層次來解釋虛擬機的工作機制。這正是我們的內聯緩存功能所需要的。 用JavaScript解釋JavaScript虛擬機-內聯緩存(inline caches) 本文轉載自:眾成翻譯譯者:LexHuang鏈接:http://www.zcfy.cc/articl...
摘要:在執行函數時,通過保存堆棧狀態,再保存堆棧跳出后返回位置的指針,最后對變量賦值。這看上去沒有問題,只要將值存在堆棧就搞定了。 1. 引言 本周精讀的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎為了優化性能,做了怎樣的嘗試吧! 這篇文章介紹的優化技術叫 preparser,是通過跳過不必要函數編譯的方式優化性能。 2. 概述 & 精讀 解析 Js 發生在網頁運行的關鍵路...
摘要:經過連續幾期的介紹,手寫編譯器系列進入了智能提示模塊,前幾期從詞法到文法語法,再到構造語法樹,錯誤提示等等,都是為智能提示做準備。 1 引言 詞法、語法、語義分析概念都屬于編譯原理的前端領域,而這次的目的是做 具備完善語法提示的 SQL 編輯器,只需用到編譯原理的前端部分。 經過連續幾期的介紹,《手寫 SQL 編譯器》系列進入了 智能提示 模塊,前幾期從 詞法到文法、語法,再到構造語法...
閱讀 3540·2023-04-25 20:09
閱讀 3743·2022-06-28 19:00
閱讀 3064·2022-06-28 19:00
閱讀 3087·2022-06-28 19:00
閱讀 3178·2022-06-28 19:00
閱讀 2883·2022-06-28 19:00
閱讀 3051·2022-06-28 19:00
閱讀 2641·2022-06-28 19:00