alxctools索引超出了数组界限_[译]V8中的数组类型
JavaScript 對象可以和任何屬性有關聯。對象屬性的名稱可以包含任何字符。有趣的是 JavaScript 引擎可以選擇名稱為純數字的屬性來進行優化,而這個屬性其實就是數組 index。
在 V8 中,會特殊處理整數名稱的屬性(最常見的形式是由 Array 構造函數生成的對象)。盡管很多情況下這些數字索引屬性的表現和其他屬性一樣,但為了優化,V8 將它們和非數字屬性分開存儲。在內部,V8 甚至給這些屬性一個特殊的名稱:elements。對象通過properties可以 map 到一些 value ,而數組通過 index 可以 map 到一些子元素。
盡管這些內部細節從來沒有直接向 JavaScript 開發人員公開,但它們解釋了為什么某些代碼模式比其他模式更快。
常見的 elements 類型
在運行 JavaScript 代碼時,V8 會追蹤每個數組的 elements 的類型。V8 可以根據這些信息,在對擁有這種 elements 類型的數組進行操作時,進行針對性的優化。例如,當在數組上調用 reduce,map 或 forEach 時,V8 可以根據數組的 elements 類型來優化這些操作。
以這個數組為例:
const array = [1, 2, 3];這個數組的 elements 類型是什么呢?如果用 typeof 來回答,結果就是這個數組含有 number 類型的數。在語言層面,這就是我們能看到的:JavaScript 不會區分整數(integers),浮點數(floats)和雙精度數(doubles),它們都只是數字。但在引擎層面,我們可以做更精確地區分。該數組的 elements 類型為 PACKED_SMI_ELEMENTS。在 V8 中,術語 Smi 是指用于存儲小整數(small integers)的一種特定格式。
然后,向同一個數組中添加浮點數會把這個數組轉為更通用的 elements 類型
const array = [1, 2, 3]; // elements 類型: PACKED_SMI_ELEMENTS array.push(4.56); // elements 類型: PACKED_DOUBLE_ELEMENTS向數組中添加字符串將再次改變數組 elements 類型
const array = [1, 2, 3]; // elements 類型: PACKED_SMI_ELEMENTS array.push(4.56); // elements 類型: PACKED_DOUBLE_ELEMENTS array.push('x'); // elements 類型: PACKED_ELEMENTS到目前為止,我們已經看到了 3 種不同的 elements 類型,以下是基本類型
- Sm all i ntegers,也就是 Smi
- Doubles,用于不能用 Smi 表示的浮點數(floating-point)和整數(integers)
- 常規 elements,用于不能表示為 Smi 或雙精度值(doubles)的值
注意,doubles 是 Smi 的一種更通用的變體,常規 elements 是 doubles 之上的另一種泛化。用 Smi 表示的數字集 是 用 double 表示的數字集的子集。
重點是 elements 類型只向一個方向轉化,從特殊的(比如: PACKED_SMI_ELEMENTS) 轉向更常規的(比如: PACKED_ELEMENTS),比如一旦一個數組被標記為是 PACKED_ELEMENTS,它就不能再轉化成 PACKED_DOUBLE_ELEMENTS 類型的了。
目前,我們已經了解到
- V8 會對每個數組賦予一個 elements 類型
- 數組的 elements 類型并不是一成不變的 —— 它可以在運行時改變。之前的例子中有從 PACKED_SMI_ELEMENTS 轉向 PACKED_ELEMENTS 的
- elements 類型只能從特定類型轉向常規類型
PACKED 和 HOLEY 類型
目前我們只談到了 packed 類型的數組。在數組中創建 holes (使數組變稀疏)會將其 elements 類型降級成它的 "holey" 版本
const array = [1, 2, 3, 4.56, 'x']; // elements 類型: PACKED_ELEMENTS array.length; // 5 array[9] = 1; // array[5] 到 array[8] 現在都是 holes // elements 類型: HOLEY_ELEMENTSV8 之所以有這種區別,它在優化 packed 類型數組的操作上比 holey 類型數組更積極。在 packed 類型數組上大多數操作都可以有效率地執行。相比之下,在 holey 類型數組上,這些操作就需要在原型鏈上進行額外的檢測,并耗費性能高昂的查詢。
到目前為止,我們已經看到每種基本 elements 類型(即 Smis,double 和常規 elements 類型)都有兩種: packed 版本和 holey 版本。它們不僅可以從 PACKED_SMI_ELEMENTS 轉變成 PACKED_DOUBLE_ELEMENTS,而且還可以從任何 PACKED 類型轉變成其 HOLEY 對應類型。 總結一下:
- 最常見的 elements 類型有 PACKED 類型和 HOLEY 類型
- 在 packed 類型數組上的操作比 holey 類型數組更有效率
- elements 類型可以從 PACKED 類型轉變成 HOLEY 類型
elements 類型 格
這套標記轉換系統被 V8 弄成了一個 格。下面是只有幾個 elements 類型的簡化示意圖:
它只能通過格來向下轉變。一旦一個 Smis 數組添加了單個浮點數(single floating-point),即便之后使用 Smi 覆蓋該浮點數,它都會被標記為 DOUBLE。同樣的,一旦一個數組中出現了 hole,即便之后將這個 hole 補上了,它都會被標記為 holey。
V8 目前區分了 21 種不同的 elements 類型,每一種都可能有一堆優化
通常,更特定的 elements 類型支持更細粒度的優化。格中 elements 類型越往下,其對象的操作就會越慢。為了獲取最佳性能,避免不太特定的類型的這種不必要的轉換,應堅持使用最適合情況的特定 elements 類型。
性能建議
大多數情況下,elements 類型的追蹤工作是在底層運行的,沒必要考慮得那么細。但為了從系統中獲取最大收益,以下幾件事情是可以做的。
避免讀取超出數組長度的內容
有點出乎意料(鑒于這篇文章的標題)的是,我們的第 1 個性能建議與 elements 類型追蹤沒有直接聯系(盡管背后發生的事情有點像)。讀取超過數組長度的數據會對性能產生驚人的影響,例如當 array.length === 5 時去讀 array[42] 的數據。這個例子中數組下標 42 已經越界,數組本身就沒這屬性,JS 引擎就會耗費昂貴的性能去原型鏈上找。一旦加載遇到這種情況,V8 會記住 "這個加載需要處理特殊情況",而且它的速度再也不會像讀取到越界之前那么快了。
不要把循環寫成這樣:
// 不要這么寫! 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 就要從原型鏈上找了。如果之后在計算中不小心使用到了這個值(也就是超出數組長度的值),影響會更糟,例如:
function Maximum(array) {let max = 0;for (let i = 0; i <= array.length; i++) { // 糟糕的比較if (array[i] > max) max = array[i];}return max; }在這里,最后一次迭代超出了數組長度,返回結果為 undefined,這既影響了加載,又影響了比較:不再只比較數字,它要處理特殊情況。把終止條件改為正確的 i < array.length 可使本示例的性能提高 6 倍(在有 10,000 個元素的數組上進行測試,迭代次數只減少了 0.01%)。
避免 elements 類型的變化
通常,如果需要在一個數組上執行很多操作,試著只用一種元素類型,盡可能是特定類型,這樣 V8 可以盡可能對這些操作進行優化。
這比看上去要難。比如僅向一個 Smi 數組中添加 -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 而言都是一樣的。它們都被看作是浮點數(doubles),所以在一個 SMI_ELEMENTS 數組中添加一個 NaN 或者是 Infinity,這個數組就會變成 DOUBLE_ELEMENTS
const array = [3, 2, 1]; // PACKED_SMI_ELEMENTS array.push(NaN, Infinity); // PACKED_DOUBLE_ELEMENTS要對一個整數數組進行大量的操作了,在它初始化時就應考慮下把 -0 變成 0,NaN 和 Infinity 之類的值就應該過濾掉。這樣一來,這個數組才會維持在 PACKED_SMI_ELEMENTS 狀態。這種一次性標準化后的開銷對于后續優化都是值得的。
實際上,如果要對數字(numbers)數組進行數學操作,可以考慮下 TypedArray。這也有對應的特定的 elements 類型。
優先使用 array 而不是 array-like 的對象
有些 JS 里的對象,特別是 DOM,看起來像是數組但其實它們并不是真正意義上的數組。創建的 array-like 的數組就像下面這樣
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 }`); }); // 先打印 '0: a', 然后打印 '1: b', 最后打印 '2: c'.這段代碼調用 array-like 對象上內置的 Array.prototype.forEach 方法,結果符合預期。但這比在真數組上調用 forEach 慢,而后者在 V8 中已被高度優化。要多次在此對象上使用內置的數組方法的話,就應先把它轉成真數組再用:
const actualArray = Array.prototype.slice.call(arrayLike, 0); actualArray.forEach((value, index) => {console.log(`${ index }: ${ value }`); }); // 先打印 '0: a', 然后打印 '1: b', 最后打印 '2: c'.這種一次性轉換的開銷對于后續的優化來講都是值得的,特別是當對數組執行大量操作時。
arguments 對象是一個 array-like 對象,可以在其上調用數組內置函數,但這種操作不會像對真數組那樣做全方位的優化。
const logArgs = function() {Array.prototype.forEach.call(arguments, (value, index) => {console.log(`${ index }: ${ value }`);}); }; logArgs('a', 'b', 'c'); // 先打印 '0: a', 然后打印 '1: b', 最后打印 '2: c'.ES2015 rest 參數可以在這里幫個忙。它們可以用真數組,而不是優雅地用 array-like 的 arguments 對象。
const logArgs = (...args) => {args.forEach((value, index) => {console.log(`${ index }: ${ value }`);}); }; logArgs('a', 'b', 'c'); // 先打印 '0: a', 然后打印 '1: b', 最后打印 '2: c'.現在你還有啥借口用 arguments 對象。
所以一般來講,盡可能避免使用 array-like 的對象,應盡可能使用真數組。
避免多態
如果代碼中要處理很多不同的 elements 類型的數組,它可能會導致多態操作,這比只用處理單個 elements 類型的代碼要慢。
看如下示例,里面調用了各種 elements 類型的庫函數。(注意下這不是原來的 Array.prototype.forEach 方法,除了本文討論的對特定 elements 類型的優化,這個示例自己也有一套優化。)
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); // `PACKED_ELEMENTS` 調用了 `each` 方法。V8 使用了內聯緩存 // (或者說叫 "IC") 記住了這個 `each` 方法是被這個 elements 類型調用的。 // 若不出意外,V8 會樂觀地假定在 `each` 方法里訪問 `array.length` 和 `array[index]` 時 // 是單一的(比如只接受一種 elements 類型),之后每次調用 `each` 方法,V8 就會去檢查這個類型 // 是不是 `PACKED_ELEMENTS`,如果是,V8 會重用之前生成的代碼; // 如果不是,就需要做更多事情了each([1.1, 2.2, 3.3], doSomething); // `PACKED_DOUBLE_ELEMENTS` 調用了 `each` 方法。 V8 此時看到,在它的內聯緩存里面, // 給 `each` 方法傳的是不同的 elements 類型的數組了,那么在 `each` 方法里訪問 `array.length` 和 `array[index]` 時就被打上了多態的標記。 // 現在每次在調用 `each` 方法時 V8 都要去做下額外的檢查: // 1. 這個是不是 `PACKED_ELEMENTS`(就像上面說過的) // 2. 這個是不是 `PACKED_DOUBLE_ELEMENTS` // 3. 這個還是不是其他的 elements 類型 // 這就會引起性能上的損耗each([1, 2, 3], doSomething); // `PACKED_SMI_ELEMENTS` 調用了 `each` 方法。這就觸發了另一個種程度的多態性。現在在內聯緩存中,對于 `each` 方法來說有 3 種不同的 elements 類型。從現在開始每次調用 `each` 方法,就需要另外檢查 elements 類型,才能將生成的代碼重新用于 `PACKED_SMI_ELEMENTS` 數組,而這都需要以消耗性能為代價才能做的。內置方法(如 Array.prototype.forEach)可以更有效地處理這種多態性,因此如果對性能敏感,請考慮使用這些內置方法而不是用戶手寫的庫函數
V8 中關于單態與多態的另一個例子就跟對象的 shape 相關,也就是對象的隱藏類。要了解更多請參考 這篇文章
避免創建 holes
在真正的代碼看來,訪問 holey 數組和 packed 數組之間的性能差異通常太小,甚至無法測量。如果性能測試表明在優化的代碼中保留每一條機器指令是值得的,那么可以嘗試把數組維持在 packed 模式。比如說,我們要創建一個數組
const array = new Array(3); // 此時這個數組是稀疏的,所以它被標記為 `HOLEY_SMI_ELEMENTS` // 根據當前的信息這就是最可能的結果array[0] = 'a'; // 等等,這是一個字符而不是一個 Smi,所以 elements 類型轉成 `HOLEY_ELEMENTS`array[1] = 'b'; array[2] = 'c'; // 此時,數組的 3 個位置都被填滿了。所以數組是 packed 了(不再是稀疏的了)。 // 然而現在已經不能把這個數組再轉成一個特定類型比如 `PACKED_ELEMENTS` 了。 // elements 類型仍然為 `HOLEY_ELEMENTS`一旦數組被標記為 holey,它將永遠保持在 holey 狀態,即便之后數組里面有元素了
創建數組的更好方法是使用如下方式
const array = ['a', 'b', 'c']; // elements 類型: PACKED_ELEMENTS如果事先不知道所有的值,可以創建一個空數組,然后將值 push 進去
const array = []; // … array.push(someValue); // … array.push(someOtherValue);這種方法確保了數組永遠不會轉換為 holey elements 類型。因此,V8 可能會為這個數組的某些操作生成更快的優化代碼。
調試 elements 類型
為了弄明白啥是對象的 elements 類型,可用 d8 的調試版本運行(通過在 debug 模式下從源碼進行構建,或使用 jsvu 弄到預編譯的二進制文件)
out/x64.debug/d8 --allow-natives-syntax這將打開一個 d8 REPL,其中可用 %DebugPrint(object) 等特殊函數。輸出的 elements 字段顯示了傳遞給 這個 debug 函數的對象的 elements 類型。
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: 11: 22: 3} […]注意,COW 表示copy-on-write,這是另一個內部優化。
在調試構建中可用的另一個有用的 flag 是 --trace-elements-transitions。用上它能讓 V8 提示你 elements 類型轉換是在啥時發生的。
$ cat my-script.js const array = [1, 2, 3]; array[3] = 4.56;$ out/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]>注:該文章翻譯自https://v8.dev/blog/elements-kindsV8 的官方博客,這是關于解釋在 V8 中「elements」的類型都有哪些的一篇文章,文章有翻譯的不是很清楚的地方,歡迎各位指正 創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的alxctools索引超出了数组界限_[译]V8中的数组类型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: curl put方法 测试http_HT
- 下一篇: mysql时间段以后_mysql时间段查