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