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-значения.

Правила быстрого кода

  1. Не смешивайте типы в массиве:
const ids = [1, 2, 3, 4];        // SMI — быстро
ids.push(1.5);                    // ВЕСЬ массив переходит в DOUBLE
  1. Используйте `` вместо new Array(n):
const arr = ;                   // PACKED_SMI начинается
for (let i = 0; i < n; i++) arr.push(i);

const arr2 = new Array(n);        // HOLEY сразу — медленнее
  1. Не используйте delete — используйте splice или фильтрацию:
delete arr[1];                    // → HOLEY навсегда
arr.splice(1, 1);                 // → возможно осталось PACKED
  1. Не пропускайте индексы:
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 — ускорение в разы

См. также