摘要:常規元素,不能表示為或雙精度的值。元素種類可從過渡轉變為。這是一個簡化的可視化,僅顯示最常見的元素種類只能通過格子向下過渡。目前有種不同的元素種類,每種元素都有自己的一組可能的優化。再次重申更具體的元素種類可以進行更細粒度的優化。
原文:“Elements kinds” in V8
JavaScript 對象可以具有與它們相關聯的任意屬性。對象屬性的名稱可以包含任何字符。JavaScript 引擎可以進行優化的一個有趣的例子是當屬性名是純數字時,一個特例就是數組索引的屬性。
在 V8 中,如果屬性名是數字(最常見的形式是 Array 構造函數生成的對象)會被特殊處理。盡管在許多情況下,這些數字索引屬性的行為與其他屬性一樣,V8 選擇將它們與非數字屬性分開存儲以進行優化。在引擎內部,V8 甚至給這些屬性一個特殊的名稱:元素。對象具有映射到值的屬性,而數組具有映射到元素的索引。
盡管這些內部結構從未直接暴露給 JavaScript 開發人員,但它們解釋了為什么某些代碼模式比其他代碼模式更快。
常見的元素種類運行 JavaScript 代碼時,V8 會跟蹤每個數組所包含的元素。這些信息可以幫助 V8 優化數組元素的操作。例如,當您在數組上調用 reduce,map 或 forEach 時,V8 可以根據數組包含哪些元素來優化這些操作。
拿這個數組舉例:
const array = [1, 2, 3];
它包含什么樣的元素?如果你使用 typeof 操作符,它會告訴你數組包含 numbers。在語言層面,這就是你所得到的:JavaScript 不區分整數,浮點數和雙精度 - 它們只是數字。然而,在引擎級別,我們可以做出更精確的區分。這個數組的元素是 PACKED_SMI_ELEMENTS。在 V8
中,術語 Smi 是指用于存儲小整數的特定格式。(后面我們會在 PACKED 部分中說明。)
稍后在這個數組中添加一個浮點數將其轉換為更通用的元素類型:
const array = [1, 2, 3]; // 元素類型: PACKED_SMI_ELEMENTS array.push(4.56); // 元素類型: PACKED_DOUBLE_ELEMENTS
向數組添加字符串再次改變其元素類型。
const array = [1, 2, 3]; // 元素類型: PACKED_SMI_ELEMENTS array.push(4.56); // 元素類型: PACKED_DOUBLE_ELEMENTS array.push("x"); // 元素類型: PACKED_ELEMENTS
到目前為止,我們已經看到三種不同的元素,具有以下基本類型:
小整數,又稱 Smi。
雙精度浮點數,浮點數和不能表示為 Smi 的整數。
常規元素,不能表示為 Smi 或雙精度的值。
請注意,雙精度浮點數是 Smi 的更為一般的變體,而常規元素是雙精度浮點數之上的另一個概括。可以表示為 Smi 的數字集合是可以表示為
double 的數字的子集。
這里重要的一點是,元素種類轉換只能從一個方向進行:從特定的(例如 PACKED_SMI_ELEMENTS)到更一般的(例如 PACKED_ELEMENTS)。例如,一旦數組被標記為 PACKED_ELEMENTS,它就不能回到 PACKED_DOUBLE_ELEMENTS。
到目前為止,我們已經學到了以下內容:
V8 為每個數組分配一個元素種類。數組的元素種類并沒有被捆綁在一起 - 它可以在運行時改變。在前面的例子中,我們從 PACKED_SMI_ELEMENTS 過渡到 PACKED_ELEMENTS。元素種類轉換只能從特定種類轉變為更普遍的種類。
PACKED vs HOLEY密集數組 PACKED 和稀疏數組 HOLEY。
到目前為止,我們只處理密集或打包(PACKED)數組。在數組中創建稀疏數組將元素降級到其 HOLEY 變體:
const array = [1, 2, 3, 4.56, "x"]; // 元素類型: PACKED_ELEMENTS array.length; // 5 array[9] = 1; // array[5] until array[8] are now holes // 元素類型: HOLEY_ELEMENTS
V8 之所以做這個區別是因為 PACKED 數組的操作比在 HOLEY 數組上的操作更利于進行優化。對于 PACKED 數組,大多數操作可以有效執行。相比之下, HOLEY 數組的操作需要對原型鏈進行額外的檢查和昂貴的查找。
到目前為止,我們看到的每個基本元素(即 Smis,double 和常規元素)有兩種:PACKED 和 HOLEY。我們不僅可以從 PACKED_SMI_ELEMENTS 轉變為 PACKED_DOUBLE_ELEMENTS 我們也可以從任何 PACKED 形式轉變成 HOLEY 形式。
回顧一下:
最常見的元素種類 PACKED 和 HOLEY。PACKED 數組的操作比在 HOLEY 數組上的操作更為有效。元素種類可從過渡 PACKED 轉變為 HOLEY。
The elements kind lattice 元素種類的格V8 將這個變換系統實現為格(數學概念)。這是一個簡化的可視化,僅顯示最常見的元素種類:
只能通過格子向下過渡。一旦將單精度浮點數添加到 Smi 數組中,即使稍后用 Smi 覆蓋浮點數,它也會被標記為 DOUBLE。類似地,一旦在數組中創建了一個洞,它將被永久標記為有洞 HOLEY,即使稍后填充它也是如此。
V8 目前有 21 種不同的元素種類,每種元素都有自己的一組可能的優化。
一般來說,更具體的元素種類可以進行更細粒度的優化。元素類型的在格子中越是向下,該對象的操作越慢。為了獲得最佳性能,請避免不必要的不具體類型 - 堅持使用符合您情況的最具體的類型。
性能提示在大多數情況下,元素種類的跟蹤操作都隱藏在引擎下面,您不需要擔心。但是,為了從系統中獲得最大的收益,您可以采取以下幾方面。再次重申:更具體的元素種類可以進行更細粒度的優化。元素類型的在格子中越是向下,該對象的操作越慢。為了獲得最佳性能,請避免不必要的不具體類型 - 堅持使用符合您情況的最具體的類型。
避免創建洞(hole)假設我們正在嘗試創建一個數組,例如:
const array = new Array(3); // 此時,數組是稀疏的,所以它被標記為 `HOLEY_SMI_ELEMENTS` // i.e. 給出當前信息的最具體的可能性。 array[0] = "a"; // 接著,這是一個字符串,而不是一個小整數...所以過渡到`HOLEY_ELEMENTS`。 array[1] = "b"; array[2] = "c"; // 這時,數組中的所有三個位置都被填充,所以數組被打包(即不再稀疏)。 // 但是,我們無法轉換為更具體的類型,例如 “PACKED_ELEMENTS”。 // 元素類保留為“HOLEY_ELEMENTS”。
一旦數組被標記為有洞,它永遠是有洞的 - 即使它被打包了!從那時起,數組上的任何操作都可能變慢。如果您計劃在數組上執行大量操作,并且希望對這些操作進行優化,請避免在數組中創建空洞。V8 可以更有效地處理密集數組。
創建數組的一種更好的方法是使用字面量:
const array = ["a", "b", "c"]; // elements kind: PACKED_ELEMENTS
如果您提前不知道元素的所有值,那么可以創建一個空數組,然后再 push 值。
const array = []; // … array.push(someValue); // … array.push(someOtherValue);
這種方法確保數組不會被轉換為 holey elements。因此,V8 可以更有效地優化數組上的任何操作。
避免讀取超出數組的長度當讀數超過數組的長度時,例如讀取 array[42] 時,會發生類似的情況 array.length === 5。在這種情況下,數組索引 42 超出范圍,該屬性不存在于數組本身上,因此 JavaScript 引擎必須執行相同的昂貴的原型鏈查找。
不要這樣寫你的循環:
// Don’t do this! for (let i = 0, item; (item = items[i]) != null; i++) { doSomething(item); }
該代碼讀取數組中的所有元素,然后再次讀取。直到它找到一個元素為 undefined 或 null 時停止。(jQuery 在幾個地方使用這種模式。)
相反,將你的循環寫成老式的方式,只需要一直迭代到最后一個元素。
for (let index = 0; index < items.length; index++) { const item = items[index]; doSomething(item); }
當你循環的集合是可迭代的(數組和 NodeLists),還有更好的選擇:只需要使用 for-of。
for (const item of items) { doSomething(item); }
對于數組,您可以使用內置的 forEach:
items.forEach((item) => { doSomething(item); });
如今,兩者的性能 for-of 和 forEach 可以和舊式的 for 循環相提并論。
避免讀數超出數組的長度!這樣做和數組中的洞一樣糟糕。在這種情況下,V8 的邊界檢查失敗,檢查屬性是否存在失敗,然后我們需要查找原型鏈。
避免元素種類轉換一般來說,如果您需要在數組上執行大量操作,請嘗試堅持盡可能具體的元素類型,以便 V8 可以盡可能優化這些操作。
這比看起來更難。例如,只需給數組添加一個 -0,一個小整數的數組即可將其轉換為 PACKED_DOUBLE_ELEMENTS。
const array = [3, 2, 1, +0]; // PACKED_SMI_ELEMENTS array.push(-0); // PACKED_DOUBLE_ELEMENTS
因此,此數組上的任何操作都將以與 Smi 完全不同的方式進行優化。
避免 -0,除非你需要在代碼中明確區分 -0 和 +0。(你可能并不需要)
同樣還有 NaN 和 Infinity。它們被表示為雙精度,因此添加一個 NaN 或 Infinity 會將 SMI_ELEMENTS 轉換為
DOUBLE_ELEMENTS。
const array = [3, 2, 1]; // PACKED_SMI_ELEMENTS array.push(NaN, Infinity); // PACKED_DOUBLE_ELEMENTS
如果您計劃對整數數組執行大量操作,在初始化的時候請考慮規范化 -0,并且防止 NaN 以及 Infinity。這樣數組就會保持 PACKED_SMI_ELEMENTS。
事實上,如果你對數組進行數學運算,可以考慮使用 TypedArray。每個數組都有專門的元素類型。
類數組對象 vs 數組JavaScript 中的某些對象 - 特別是在 DOM 中 - 雖然它們不是真正的數組,但是他們看起來像數組。可以自己創建類數組的對象:
const arrayLike = {}; arrayLike[0] = "a"; arrayLike[1] = "b"; arrayLike[2] = "c"; arrayLike.length = 3;
該對象具有 length 并支持索引元素訪問(就像數組!),但它的原型上缺少數組方法,如 forEach。盡管如此,仍然可以調用數組泛型:
Array.prototype.forEach.call(arrayLike, (value, index) => { console.log(`${ index }: ${ value }`); }); // This logs "0: a", then "1: b", and finally "2: c".
這個代碼工作原理如下,在類數組對象上調用數組內置的 Array.prototype.forEach。但是,這比在真正的數組中調用 forEach 慢,引擎數組的 forEach 在 V8 中是高度優化的。如果你打算在這個對象上多次使用數組內置函數,可以考慮先把它變成一個真正的數組:
const actualArray = Array.prototype.slice.call(arrayLike, 0); actualArray.forEach((value, index) => { console.log(`${ index }: ${ value }`); }); // This logs "0: a", then "1: b", and finally "2: c".
為了后續的優化,進行一次性轉換的成本是值得的,特別是如果您計劃在數組上執行大量操作。
例如,arguments 對象是類數組的對象。可以在其上調用數組內置函數,但是這樣的操作將不會被完全優化,因為這些優化只針對真正的數組。
const logArgs = function() { Array.prototype.forEach.call(arguments, (value, index) => { console.log(`${ index }: ${ value }`); }); }; logArgs("a", "b", "c"); // This logs "0: a", then "1: b", and finally "2: c".
ES2015 的 rest 參數在這里很有幫助。它們產生真正的數組,可以優雅的代替類似數組的對象 arguments。
const logArgs = (...args) => { args.forEach((value, index) => { console.log(`${ index }: ${ value }`); }); }; logArgs("a", "b", "c"); // This logs "0: a", then "1: b", and finally "2: c".
如今,沒有理由直接使用對象 arguments。
通常,盡可能避免使用數組類對象,應該使用真正的數組。
避免多態如果您的代碼需要處理包含多種不同元素類型的數組,則可能會比單個元素類型數組要慢,因為你的代碼要對不同類型的數組元素進行多態操作。
考慮以下示例,其中使用了各種元素種類調用。(請注意,這不是本機 Array.prototype.forEach,它具有自己的一些優化,這些優化不同于本文中討論的元素種類優化。)
const each = (array, callback) => { for (let index = 0; index < array.length; ++index) { const item = array[index]; callback(item); } }; const doSomething = (item) => console.log(item); each([], () => {}); each(["a", "b", "c"], doSomething); // `each` is called with `PACKED_ELEMENTS`. V8 uses an inline cache // (or “IC”) to remember that `each` is called with this particular // elements kind. V8 is optimistic and assumes that the // `array.length` and `array[index]` accesses inside the `each` // function are monomorphic (i.e. only ever receive a single kind // of elements) until proven otherwise. For every future call to // `each`, V8 checks if the elements kind is `PACKED_ELEMENTS`. If // so, V8 can re-use the previously-generated code. If not, more work // is needed. each([1.1, 2.2, 3.3], doSomething); // `each` is called with `PACKED_DOUBLE_ELEMENTS`. Because V8 has // now seen different elements kinds passed to `each` in its IC, the // `array.length` and `array[index]` accesses inside the `each` // function get marked as polymorphic. V8 now needs an additional // check every time `each` gets called: one for `PACKED_ELEMENTS` // (like before), a new one for `PACKED_DOUBLE_ELEMENTS`, and one for // any other elements kinds (like before). This incurs a performance // hit. each([1, 2, 3], doSomething); // `each` is called with `PACKED_SMI_ELEMENTS`. This triggers another // degree of polymorphism. There are now three different elements // kinds in the IC for `each`. For every `each` call from now on, yet // another elements kind check is needed to re-use the generated code // for `PACKED_SMI_ELEMENTS`. This comes at a performance cost.
內置方法(如 Array.prototype.forEach)可以更有效地處理這種多態性,因此在性能敏感的情況下考慮使用它們而不是用戶庫函數。
V8 中單態與多態的另一個例子涉及對象形狀(object shape),也稱為對象的隱藏類。要了解更多,請查看 Vyacheslav 的文章。
調試元素種類找出一個給定的對象的“元素種類”,可以使用一個調試版本 d8(參見“從源代碼構建”),并運行:
$ out.gn/x64.debug/d8 --allow-natives-syntax
這將打開 d8 REPL 中的特殊函數,如 %DebugPrint(object)。輸出中的“元素”字段顯示您傳遞給它的任何對象的“元素種類”。
d8> const array = [1, 2, 3]; %DebugPrint(array); DebugPrint: 0x1fbbad30fd71: [JSArray] - map = 0x10a6f8a038b1 [FastProperties] - prototype = 0x1212bb687ec1 - elements = 0x1fbbad30fd19[PACKED_SMI_ELEMENTS (COW)] - length = 3 - properties = 0x219eb0702241 { #length: 0x219eb0764ac9 (const accessor descriptor) } - elements= 0x1fbbad30fd19 { 0: 1 1: 2 2: 3 } […]
請注意,“COW” 表示寫時復制,這是另一個內部優化。現在不要擔心 - 這是另一個博文的主題!
調試版本中可用的另一個有用的標志是 --trace-elements-transitions。啟用它讓 V8 在任何元素發生類型轉換時通知您。
$ cat my-script.js const array = [1, 2, 3]; array[3] = 4.56; $ out.gn/x64.debug/d8 --trace-elements-transitions my-script.js elements transition [PACKED_SMI_ELEMENTS -> PACKED_DOUBLE_ELEMENTS] in ~+34 at x.js:2 for 0x1df87228c911from 0x1df87228c889 to 0x1df87228c941
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88587.html
摘要:與異步編程按照維基百科上的解釋獨立于主控制流之外發生的事件就叫做異步。因為的存在,至少在被標準化的那一刻起,就支持異步編程了。然而異步編程真正發展壯大,的流行功不可沒。在握手過程中,端點交換認證和密鑰以建立或恢復安全會話。 1、前端 排序算法總結 排序算法可能是你學編程第一個學習的算法,還記得冒泡嗎? 當然,排序和查找兩類算法是面試的熱門選項。如果你是一個會寫快排的程序猿,面試官在比較...
摘要:與異步編程按照維基百科上的解釋獨立于主控制流之外發生的事件就叫做異步。因為的存在,至少在被標準化的那一刻起,就支持異步編程了。然而異步編程真正發展壯大,的流行功不可沒。在握手過程中,端點交換認證和密鑰以建立或恢復安全會話。 1、前端 排序算法總結 排序算法可能是你學編程第一個學習的算法,還記得冒泡嗎? 當然,排序和查找兩類算法是面試的熱門選項。如果你是一個會寫快排的程序猿,面試官在比較...
摘要:與異步編程按照維基百科上的解釋獨立于主控制流之外發生的事件就叫做異步。因為的存在,至少在被標準化的那一刻起,就支持異步編程了。然而異步編程真正發展壯大,的流行功不可沒。在握手過程中,端點交換認證和密鑰以建立或恢復安全會話。 1、前端 排序算法總結 排序算法可能是你學編程第一個學習的算法,還記得冒泡嗎? 當然,排序和查找兩類算法是面試的熱門選項。如果你是一個會寫快排的程序猿,面試官在比較...
摘要:可以更有效地處理密集數組。然后有人提出了一個疑問為什么先指定長度再初始化測試出來會快一點其實,兩者相比只是可能變慢。具體因素有很多,比如預分配一個很大的數組,這時可以變快,的函數就是這么做的。如果數組很大,預先分配大小后性能反而會提升。 在我的上一篇文章 JavaScript 在 V8 中的元素種類及性能優化 中寫道: showImg(https://segmentfault.com...
摘要:這些是中可用的最快屬性。通常來說我們將線性屬性存儲中存儲的屬性稱為。因此也支持所謂的屬性。整數索引屬性的處理和命名屬性的復雜性相同。 本文為譯文,原文地址:http://v8project.blogspot.com...,作者,@Camillo Bruni ,V8 JavaScript Engine Team Blog 在這篇博客中,我們想解釋 V8 如何在內部處理 JavaScrip...
閱讀 882·2021-11-23 09:51
閱讀 1089·2021-11-15 17:57
閱讀 1667·2021-09-22 15:24
閱讀 812·2021-09-07 09:59
閱讀 2221·2019-08-29 15:10
閱讀 1849·2019-08-29 12:47
閱讀 751·2019-08-29 12:30
閱讀 3369·2019-08-26 13:51