open atlas
↑ К треку
Внутренности движка JavaScript JSE · 03 · 03

Быстрые свойства, медленные свойства и slack tracking

Три режима хранения: in-object-свойства (встроены в тело, один load), out-of-object fast properties (PropertyArray, +1 разыменование) и dictionary mode (хешмапа NameDictionary, ~50-100 циклов, включается delete'ом или избытком свойств).

JSE Middle ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

Вы создаёте класс с тремя полями, но самые первые экземпляры, которые V8 аллоцирует, крупнее, чем нужно, — дополнены пустыми слотами. Затем, после нескольких сотен экземпляров, V8 тихо ужимает класс и освобождает дополнение. Это slack tracking, и если вы не знали о его существовании, вы неверно читали снимки кучи, снятые во время разогрева. Хранение свойств в V8 имеет три режима и один самонастраивающийся аллокатор, и то, в какой из них попадёт ваш объект, решает, будет ли чтение одной инструкцией или хеш-зондом.

Три режима хранения, три скорости

Почему место хранения так важно? Потому что разрыв в 50-100 раз между режимами — это именно тот обрыв, который виден как плоская полка на flame chart CPU, но исчезает при микроаллокациях. Его можно найти только если знаешь, что искать, ещё до профилирования.

DescriptorArray из урока 01 сообщает V8, где живёт поле. Возможных ответов три, в порядке убывания скорости:

  1. In-object-свойства. Хранятся встроенно в собственном блоке памяти объекта, сразу за заголовком. Чтение одного — это единственный load по постоянному смещению, самое быстрое из возможного. Число in-object-слотов фиксируется при создании Map.
  2. Out-of-object («normal») fast properties. Когда объект перерастает свои in-object-слоты, дальнейшие свойства проливаются в отдельный PropertyArray, на который объект указывает. Они всё ещё быстрые (фиксированное смещение, дружелюбны к IC), но стоят одного лишнего разыменования указателя: загрузить указатель PropertyArray, затем загрузить слот. DescriptorArray записывает индекс со смыслом «это поле в PropertyArray по индексу N».
  3. Dictionary mode (медленные свойства). Когда объект становится слишком динамичным — слишком много свойств или любой delete — V8 полностью отказывается от раскладки с фиксированными смещениями и переводит объект в NameDictionary: пер-объектную хеш-таблицу из имени свойства в значение+attributes. Каждый доступ теперь — общий поиск по хешу, примерно 50–100 циклов против ~1 для in-object-load, и inline cache не может помочь. %HasFastProperties(obj) возвращает false.
// d8 --allow-natives-syntax
const a = { p: 1, q: 2, r: 3 };
%HasFastProperties(a);   // true  — in-object, фиксированные смещения
delete a.q;
%HasFastProperties(a);   // false — теперь NameDictionary, навсегда медленный

Slack tracking: V8 подгоняет размер ваших объектов

Вот самонастраивающаяся часть, которую обзор пропустил. Когда вы определяете конструктор или класс, V8 ещё не знает, со сколькими свойствами экземпляры окажутся в итоге — за this.x = ...; this.y = ... могут последовать другие присваивания в методах, вызванных позже. Аллоцировать ровно два in-object-слота заранее значило бы вынудить пролив в PropertyArray в момент появления третьего поля.

Поэтому V8 переаллоцирует: начальный Map конструктора резервирует лишние in-object-слоты (slack) — больше, чем объявляет тело конструктора. По мере создания и работы экземпляров V8 следит за максимальным числом in-object-свойств, которые они реально используют. После того как аллоцировано достаточно экземпляров (фиксированный порог по числу созданий), он завершает slack tracking: подрезает неиспользованные слоты, ужимает instance size в Map и финализирует Map. С этого момента каждый новый экземпляр аллоцируется в подрезанном, точном размере.

Последствия для сеньора:

  • Экземпляры разогрева крупнее экземпляров установившегося состояния. Снимок кучи, снятый до завершения slack tracking, завышает размер на объект. Измеряйте после разогрева.
  • Map не «финален» сразу. Код, зависящий от стабилизированной формы (и специализации TurboFan), получает её лишь после завершения slack tracking. Микробенчмарки, аллоцирующие горстку объектов, могут видеть другую форму, чем прод в масштабе.
  • Добавление свойств вне конструктора после финализации уже не помещается в in-object-область и проливается в PropertyArray (или, если достаточно динамично, в dictionary mode).

Что толкает объект в dictionary mode

Dictionary mode — это обрыв. Вы падаете с него через:

  • delete obj.prop на быстром объекте — самая частая причина. V8 не может оставить дырку в раскладке с фиксированными смещениями, поэтому конвертирует весь объект в NameDictionary. Это навсегда: объект никогда не поднимается обратно к fast properties.
  • Слишком много свойств, добавленных динамически (порог большой, порядка многих сотен до ~1000+, и зависит от версии), так что это редко настоящий виновник — виновник delete.
  • Добавление свойств способом, который ломает дерево переходов — экстремальный churn форм может заставить V8 сдаться и переключиться в dictionary mode, чтобы перестать аллоцировать Map.

Заметьте, const-folding взаимодействует тут: когда поле const (по экземплярам присвоено лишь одно значение), V8 может хранить значение в Map (в дескрипторе), а не в объекте, так что чтения становятся «загрузить константу из Map» — но первое расходящееся присваивание теряет constness (урок 02), и значение переезжает в настоящее поле. Dictionary mode отказывается от всего этого.

Стоимость хранения свойств (V8, 64 бита)
In-object-чтение
1 load (~1 цикл)
Чтение PropertyArray
2 load'а (+1 разыменование)
Чтение в dictionary mode
~50-100 циклов, хеш-зонд
Что включает dictionary mode
delete или экстремальная динамичность
Выход из dictionary mode
никакого — нужен свежий объект
Slack tracking завершается после
фиксированного порога по числу созданий
Определить режим
%HasFastProperties(obj) в d8
Викторина

Чтение свойства компилируется в «загрузить указатель PropertyArray, затем загрузить слот 2 этого массива». В каком режиме хранения это свойство?

Викторина

Снимок кучи после аллокации 10 экземпляров класса показывает, что каждый экземпляр крупнее, чем в снимке после аллокации 10000. Почему?

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

Расставьте по порядку жизненный цикл slack tracking для экземпляров конструктора.

  1. 1 Начальный Map резервирует лишние in-object-слоты сверх того, что объявляет тело конструктора
  2. 2 Экземпляры аллоцируются; V8 записывает максимум реально использованных in-object-свойств
  3. 3 Достигнут фиксированный порог по числу созданных экземпляров
  4. 4 V8 подрезает неиспользованные in-object-слоты и ужимает instance size
  5. 5 Map финализируется; каждый последующий экземпляр аллоцируется в подрезанном точном размере
Частая ошибка

Частая самопричинённая рана dictionary mode: использование обычного объекта как хешмапы с ключами от пользователя (cache[userId] = entry), а затем delete cache[userId] для вытеснения. Первый delete конвертирует cache в dictionary mode — что на самом деле корректно для настоящей мапы, но вы также теряете любую выгоду IC на нём и платите стоимость хеш-зонда на каждый доступ. Если объект концептуально словарь, используйте настоящий Map с самого начала: он создан для произвольных ключей и удалений, никогда не плодит hidden class и имеет предсказуемую производительность.

Вспомните перед уходом
  1. 01
    Опишите три режима хранения свойств и стоимость чтения в каждом.
  2. 02
    Что такое slack tracking и почему размер объекта надо измерять после разогрева?
  3. 03
    Почему `delete` хуже присваивания null в терминах хранения?
Итог

V8 хранит именованные свойства объекта в одном из трёх режимов. In-object-свойства лежат встроенно в теле объекта и читаются единственным load’ом по постоянному смещению. Когда объект перерастает свои in-object-слоты, лишние свойства проливаются в out-of-object PropertyArray, стоя одного дополнительного разыменования указателя на чтение, но оставаясь с фиксированным смещением и дружелюбными к IC. Dictionary mode — это обрыв: запускаемый главным образом delete (и экстремальной динамичностью), он меняет фиксированную раскладку на пер-объектную хеш-таблицу NameDictionary, делая каждый доступ хеш-зондом ~50-100 циклов без помощи inline cache, и это навсегда — лишь свежий объект восстанавливает fast properties. Slack tracking — самонастраивающийся аллокатор V8 для быстрых объектов: начальный Map конструктора резервирует лишние in-object-слоты, V8 наблюдает, сколько экземпляры реально используют по мере создания, и после фиксированного порога по числу созданий подрезает slack, ужимает instance size и финализирует Map — так что память на объект устанавливается лишь после разогрева. Определяйте режим через %HasFastProperties(obj), предпочитайте null вместо delete и берите настоящий Map, когда ключи действительно динамичны. Теперь, когда увидишь false от %HasFastProperties на горячем объекте или снимок кучи, где размеры объектов уменьшаются после разогрева, ты знаешь, с какого слоя начинать разбор.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки
встречается в184

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.