V8 Element Kinds (внутренние типы массивов)
V8 хранит массивы в одной из ~20 форм (Element Kinds) в зависимости от типа и плотности элементов. Переходы между формами одностороннее: только от быстрых к медленным.
Что это / Зачем
Простой массив [1, 2, 3] в V8 — это не "массив значений Number", а массив SMI (Small Integer) в специальном внутреннем представлении. Если добавить arr.push(1.5) — массив целиком переходит в форму PACKED_DOUBLE. Один delete arr[5] навсегда переводит в HOLEY. Эти переходы влияют на скорость в 2-10 раз.
Основные Element Kinds
Решётка переходов (от быстрого к медленному):
PACKED_SMI_ELEMENTS ← [1, 2, 3]
↓ (добавили 1.5)
PACKED_DOUBLE_ELEMENTS ← [1, 2, 3, 1.5]
↓ (добавили "string" или объект)
PACKED_ELEMENTS ← [1, 2, 3, 'a']
↓ (любой delete или sparse операция)
HOLEY_*_ELEMENTS ← [1, , 3]
↓ (массив больше ~32k и разреженный)
DICTIONARY_ELEMENTS ← разреженный hashmap
PACKED = нет дырок, все индексы 0..length-1 заполнены. HOLEY = есть дырки.
Что переводит массив в HOLEY (необратимо!)
const arr = [1, 2, 3]; // PACKED_SMI
arr[10] = 99; // → HOLEY_SMI (дырки 3..9)
const arr2 = new Array(5); // сразу HOLEY (длина без значений)
const arr3 = [1, 2, 3]; // PACKED_SMI
delete arr3[1]; // → HOLEY_SMI
const arr4 = [1, 2, 3]; // PACKED_SMI
arr4.length = 10; // → HOLEY_SMI
Что переводит SMI → DOUBLE → ELEMENTS
const arr = [1, 2, 3]; // PACKED_SMI_ELEMENTS
arr.push(1.5); // → PACKED_DOUBLE_ELEMENTS
arr.push('hello'); // → PACKED_ELEMENTS (general)
SMI = 31-bit signed integer (V8 на 64-bit). Хранится прямо без heap allocation. DOUBLE = IEEE 754, хранится в heap (HeapNumber), но в массиве может быть unboxed. ELEMENTS = generic — указатели на любые JS-значения.
Правила быстрого кода
- Не смешивайте типы в массиве:
const ids = [1, 2, 3, 4]; // SMI — быстро
ids.push(1.5); // ВЕСЬ массив переходит в DOUBLE
- Используйте `` вместо
new Array(n):
const arr = ; // PACKED_SMI начинается
for (let i = 0; i < n; i++) arr.push(i);
const arr2 = new Array(n); // HOLEY сразу — медленнее
- Не используйте
delete— используйтеspliceили фильтрацию:
delete arr[1]; // → HOLEY навсегда
arr.splice(1, 1); // → возможно осталось PACKED
- Не пропускайте индексы:
arr[1000] = 1; // если до этого length=0 → HOLEY
TypedArrays — отдельная история
Int32Array, Float64Array и т.д. — фиксированный тип, всегда unboxed, без element kind transitions. Используйте для математики и бинарных данных.
Подводные камни
Array.from({length: n})→ HOLEY[...iter]сохраняет PACKED, если iter иммутабельный.map,.filterсоздают НОВЫЙ массив со своим element kind- Sparse-методы (
Array.prototype.forEach) пропускают holes,for— нет
🎓 Источники
- ⚡ [How to get maximum performance when working with JS arrays] · AsForJS · 2021-11-29 · YouTube
- Тезисы: SMI vs DOUBLE vs ELEMENTS. Переход в HOLEY необратим. delete — главный враг производительности массивов
- ⚡ [Производительность JavaScript Array в V8 perf5] · AsForJS · 2024-04-06 · YouTube
- Тезисы: дерево element kinds, transitions, влияние на JIT
- ⚡ [Разбор вопроса о Array Double vs Array SMI] · AsForJS · 2023-06-23 · YouTube
- Тезисы: добавление 0.5 в массив int — переход всего массива в DOUBLE
- ⚡ [The second part about Zyuzka] · AsForJS · 2021-04-15 · YouTube
- Тезисы: переписать пример массива с учётом element kinds — ускорение в разы