awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

Hidden classes, деревья переходов и расположение в памяти

Суть Как V8 отслеживает формы объектов через hidden classes, почему порядок добавления свойств важен, слоты in-object vs out-of-object, виды элементов массивов, строковая интернализация и ловушка dictionary mode.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Два объекта имеют одинаковые свойства. Один создан через {x:1, y:2}, другой через {y:2, x:1}. Для вашего кода они выглядят идентично. Для V8 это разные формы — и какой из них попадёт в горячую функцию первым, решит, будет ли эта функция работать быстро или медленно.

Что такое hidden class

JavaScript-объекты динамические — свойства можно добавлять, удалять, переупорядочивать — но V8 внутренне представляет каждый объект как указатель на hidden class (называемый «Map» в исходниках V8, «Shape» в SpiderMonkey) плюс последовательность слотов свойств. Два объекта с одинаковыми именами свойств, добавленными в одинаковом порядке, разделяют один hidden class.

При добавлении свойства к существующему объекту V8 переходит к новому hidden class по «дереву переходов». Добавление «x» затем «y» ведёт к одному классу; добавление «y» затем «x» — к другому, хотя оба объекта имеют одинаковое логическое содержимое. Inline cache ключуется по указателю hidden class. Стабильные hidden classes — фундамент производительности V8.

Правила:

  • Конструируйте объекты со всеми свойствами заранее (в конструкторах или фабричных функциях).
  • Избегайте delete на свойствах объекта — он вынуждает переход в dictionary mode с постоянным замедлением.
Hidden class и числа расположения в памяти
Слоты in-object (по умолчанию)
~8 слотов
Out-of-object: дополнительное разыменование
+1 загрузка памяти
Порог dictionary mode (~удалений)
~32+ свойства или много delete
Диапазон Smi (64-бит)
±2³¹
Стоимость Smi-арифметики
1 инструкция CPU, без аллокации
Стоимость HeapNumber
1 аллокация в куче + разыменование

Дерево переходов

const p = {}; p.x = 1; p.y = 2;
// Путь HC: HC_empty → HC_x → HC_x+y

const q = {}; q.x = 1; q.y = 2;
// Тот же путь — q заканчивается в том же HC, что p. ✓ monomorphic

const r = {}; r.y = 2; r.x = 1;
// Путь HC: HC_empty → HC_y → HC_y+x
// Другой лист — другой HC от p и q. ✗ polymorphic на общем call site

Дерево переходов разделяется всеми объектами в V8 isolate. Патологический код, добавляющий свойства в динамическом порядке (JSON-парсеры, записывающие свойства в произвольном порядке входных данных, генерируемый код), создаёт много листовых узлов — одна форма никогда не повторяется, IC постоянно polymorphic или megamorphic.

In-object vs out-of-object свойства

V8 хранит первые ~8 свойств объекта встроенно в блок памяти объекта. Последующие свойства идут во внешний PropertyArray, на который ссылается объект. Доступ к in-object свойствам — одна загрузка памяти; out-of-object добавляет одно разыменование указателя.

Количество in-object слотов фиксируется при создании hidden class. Для производительно-критичных объектов объявляйте наиболее используемые свойства первыми в конструкторе, чтобы они попали in-object.

Dictionary mode: объекты с слишком большим количеством свойств (~32+) или с многочисленными удалениями переходят в HashMap-представление, где каждый доступ проходит через медленный общий путь. После перехода в dictionary mode объект навсегда медленный — %HasFastProperties(obj) в d8 возвращает false.

Виды элементов массивов (Array element kinds)

V8 отслеживает «вид» каждого массива на основе того, что в нём хранится. Переходы односторонние — раз перешёл к более широкому виду, вернуться нельзя:

ВидСодержимоеСкорость
PACKED_SMI_ELEMENTSтолько smi-целые, без пробеловсамая быстрая
PACKED_DOUBLE_ELEMENTSтолько double, без пробеловбыстрая
PACKED_ELEMENTSсмешанные типы или объектынормальная
HOLEY_* вариантыразреженный массив (есть пробелы)добавляет проверку на «дыру» при каждом доступе

new Array(1000) создаёт HOLEY-массив с самого начала. Array.from({length:1000}, () => 0) создаёт PACKED-массив. Тот же логический результат, в 2–3 раза разная скорость доступа.

Чтобы держать массив PACKED_SMI: предварительно выделяйте через [], добавляйте через push, никогда не присваивайте по индексам выше текущей длины. Диагностика: %DebugPrint(arr) в d8 показывает kind.

String internalization и сравнение

V8 поддерживает внутренний пул для маленьких строковых литералов и имён свойств. Две интернированные строки сравниваются как равенство указателей (1 цикл), а не побайтово. Доступ к свойству всегда интернирует ключ. Конкатенация ('a' + 'b') создаёт ConsString — дерево без копирования. Подстрока создаёт SlicedString, удерживающий ссылку на родителя. Это даёт O(1) подстроку/конкатенацию, но родительская строка живёт в куче пока существует хоть один slice. str.slice() + '' или явное копирование разрывает ссылку, освобождая родителя. Для строко-интенсивных нагрузок (парсеры, шаблонизаторы) знание этого паттерна — разница между O(n) и O(n²) в работе с памятью.

Числовая специализация: Smi, HeapNumber, double

V8 хранит малые целые (Smi) встроенно в 64-битный слот указателя с очищенным младшим битом — без аллокации, без косвенности, арифметика за одну инструкцию CPU. Числа вне диапазона Smi (±2³¹ на 64-бит) становятся HeapNumbers — упакованными в аллоцированную в куче ячейку с double внутри. Переход Smi-в-HeapNumber — самый частый триггер deopt TurboFan: скомпилированный цикл предполагает Smi, значение один раз переполняется, каскад deopt. Решение: ограничить диапазон целых или использовать Math.fround / явный Float64Array.

Викторина

Два объекта: `{x:1, y:2}` и `{y:2, x:1}`. Функция обращается к `.x` на обоих. Какое состояние IC достигает точка доступа?

Расставь шаги по порядку

Упорядочите переходы hidden class для: const o = {}; o.x = 1; o.y = 2; o.z = 3;

  1. 1 HC_empty (объект создан без свойств)
  2. 2 HC_x (свойство 'x' добавлено)
  3. 3 HC_x+y (свойство 'y' добавлено)
  4. 4 HC_x+y+z (свойство 'z' добавлено)
Викторина

Почему `new Array(1000)` создаёт более медленный массив, чем `[]` с последующими 1000 push?

Граничные случаи

String internalization: V8 поддерживает внутренний пул для маленьких строковых литералов и имён свойств. Две интернированные строки сравниваются как равенство указателей (1 цикл), а не побайтово. Конкатенация создаёт ConsString — плоское дерево без копирования. Подстрока создаёт SlicedString, удерживающий ссылку на родительскую строку. Это даёт O(1) подстрока/конкатенация, но родительская строка живёт в куче пока существует хоть один slice. Для чувствительного к памяти кода str.slice() + '' или явное копирование разрывает ссылку, освобождая родителя.

Вспомните перед уходом
  1. 01
    Что такое «дерево переходов hidden class» и почему порядок свойств при создании важен?
  2. 02
    Что такое 'dictionary mode' и как его вызвать?
  3. 03
    Почему переход Smi-в-HeapNumber вызывает deopt в TurboFan?
Итог

Каждый объект V8 несёт указатель на hidden class, точно описывающий, какие свойства существуют и по каким смещениям памяти. При добавлении свойства к объекту V8 следует или создаёт ребро перехода в дереве переходов hidden class — путь зависит от порядка добавления, а не от итогового набора свойств. Два объекта с одинаковыми свойствами, добавленными в разном порядке, находятся в разных листовых узлах и выглядят как разные формы для любого inline cache. In-object свойства (первые ~8) стоят одной загрузки памяти; out-of-object — двух. Dictionary mode — вызываемый delete или чрезмерным динамическим добавлением свойств — заменяет расположение с фиксированными смещениями на hashmap и постоянен. Виды элементов массивов следуют одностороннему расширению: PACKED_SMI самый быстрый, HOLEY-варианты требуют проверки на пробел при каждом доступе. Smi-арифметика не требует аллокации; выход за диапазон Smi создаёт HeapNumber и вызывает deopt TurboFan.

Связанные уроки
встречается в162
Продолжить восхождение ↑Inline caches, состояния IC и деоптимизация
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.