原文:「Elements kinds」 in V8javascript
JavaScript 對象能夠具備與它們相關聯的任意屬性。對象屬性的名稱能夠包含任何字符。JavaScript 引擎能夠進行優化的一個有趣的例子是當屬性名是純數字時,一個特例就是數組索引的屬性。html
在 V8 中,若是屬性名是數字(最多見的形式是 Array 構造函數生成的對象)會被特殊處理。儘管在許多狀況下,這些數字索引屬性的行爲與其餘屬性同樣,V8 選擇將它們與非數字屬性分開存儲以進行優化。在引擎內部,V8 甚至給這些屬性一個特殊的名稱:元素。對象具備映射到值的屬性,而數組具備映射到元素的索引。java
儘管這些內部結構從未直接暴露給 JavaScript 開發人員,但它們解釋了爲何某些代碼模式比其餘代碼模式更快。git
運行 JavaScript 代碼時,V8 會跟蹤每一個數組所包含的元素。這些信息能夠幫助 V8 優化數組元素的操做。例如,當您在數組上調用 reduce
,map
或 forEach
時,V8 能夠根據數組包含哪些元素來優化這些操做。github
拿這個數組舉例:shell
const array = [1, 2, 3];
它包含什麼樣的元素?若是你使用 typeof
操做符,它會告訴你數組包含 numbers
。在語言層面,這就是你所獲得的:JavaScript 不區分整數,浮點數和雙精度 - 它們只是數字。然而,在引擎級別,咱們能夠作出更精確的區分。這個數組的元素是 PACKED_SMI_ELEMENTS。在 V8
中,術語 Smi 是指用於存儲小整數的特定格式。(後面咱們會在 PACKED
部分中說明。)數組
稍後在這個數組中添加一個浮點數將其轉換爲更通用的元素類型:ide
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 的數字集合是能夠表示爲
double 的數字的子集。
這裏重要的一點是,元素種類轉換隻能從一個方向進行:從特定的(例如 PACKED_SMI_ELEMENTS
)到更通常的(例如 PACKED_ELEMENTS
)。例如,一旦數組被標記爲 PACKED_ELEMENTS
,它就不能回到 PACKED_DOUBLE_ELEMENTS
。
到目前爲止,咱們已經學到了如下內容:
V8 爲每一個數組分配一個元素種類。數組的元素種類並無被捆綁在一塊兒 - 它能夠在運行時改變。在前面的例子中,咱們從 PACKED_SMI_ELEMENTS
過渡到 PACKED_ELEMENTS
。元素種類轉換隻能從特定種類轉變爲更廣泛的種類。
密集數組 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
。
V8 將這個變換系統實現爲格(數學概念)。這是一個簡化的可視化,僅顯示最多見的元素種類:
只能經過格子向下過渡。一旦將單精度浮點數添加到 Smi 數組中,即便稍後用 Smi 覆蓋浮點數,它也會被標記爲 DOUBLE
。相似地,一旦在數組中建立了一個洞,它將被永久標記爲有洞 HOLEY
,即便稍後填充它也是如此。
V8 目前有 21 種不一樣的元素種類,每種元素都有本身的一組可能的優化。
通常來講,更具體的元素種類能夠進行更細粒度的優化。元素類型的在格子中越是向下,該對象的操做越慢。爲了得到最佳性能,請避免沒必要要的不具體類型 - 堅持使用符合您狀況的最具體的類型。
在大多數狀況下,元素種類的跟蹤操做都隱藏在引擎下面,您不須要擔憂。可是,爲了從系統中得到最大的收益,您能夠採起如下幾方面。再次重申:更具體的元素種類能夠進行更細粒度的優化。元素類型的在格子中越是向下,該對象的操做越慢。爲了得到最佳性能,請避免沒必要要的不具體類型 - 堅持使用符合您狀況的最具體的類型。
假設咱們正在嘗試建立一個數組,例如:
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
。每一個數組都有專門的元素類型。
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 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)] - length = 3 - properties = 0x219eb0702241 <FixedArray[0]> { #length: 0x219eb0764ac9 <AccessorInfo> (const accessor descriptor) } - elements= 0x1fbbad30fd19 <FixedArray[3]> { 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 0x1df87228c911 <JSArray[3]> from 0x1df87228c889 <FixedArray[3]> to 0x1df87228c941 <FixedDoubleArray[22]>