Дисциплина мономорфизма
Действенный чек-лист, связывающий весь трек: строй объекты с полями в фиксированном порядке, никогда не delete, держи массивы в одном element kind, передавай в горячие функции аргументы одной формы, держи числа в диапазоне Smi и избегай megamorphic-диспетчеризации
У каждого цикла деопта, megamorphic-точки вызова и объекта в словарном режиме, которые вы трассировали в прошлом уроке, один корень: движок сделал ставку на форму или тип, а ваш код поменял её под ногами оптимизированного кода. Этот урок — обратная сторона: малый набор привычек, которые держат эти ставки выигрышными. Ни одна из них не микрооптимизация. Это то, как пишут JavaScript, остающийся на быстрых путях V8 по умолчанию.
Один принцип, шесть правил
Когда понятно, откуда берутся деопты и megamorphic-точки, следующий вопрос: какие привычки не дают им возникнуть с самого начала? Ответ удивительно компактен.
Оптимизированный код V8 спекулятивен: TurboFan компилирует функцию, предполагая, что формы и типы, которые он видел до сих пор, продолжат приходить. Сломайте это предположение — и платите: деопт, polymorphic IC, падение в словарный режим, откат из-за переполнения Smi. Дисциплина ниже — это просто «не удивляй оптимизатор». Каждое правило отображается на механизм, уже встреченный в этом треке (mono/poly/mega IC, переходы форм, element kinds, Smi против HeapNumber); здесь они становятся чек-листом.
Правило 1 — создавай со всеми полями в фиксированном порядке
Стройте объекты так, чтобы каждый экземпляр шёл по одному и тому же пути переходов hidden class: объявляйте все поля в конструкторе или фабрике, в постоянном порядке, без условных добавлений. Два объекта с одними полями, добавленными в разном порядке, — это разные формы; поле, добавляемое лишь иногда, создаёт вторую форму. Фикс для отсутствующих данных — значение по умолчанию, а не пропуск:
// НЕ ТАК — условия + поля добавляются позже → много форм
function makeRow(r) {
const o = {};
o.id = r.id;
if (r.name) o.name = r.name; // форма A vs форма B
o.score = r.score;
if (r.flagged) o.flag = true; // формы C, D...
return o;
}
// ТАК — одна форма, всегда
function makeRow(r) {
return {
id: r.id,
name: r.name ?? null,
score: r.score,
flag: r.flagged === true,
};
}Это единственное правило предотвращает самую частую продакшен-регрессию: горячая точка доступа становится megamorphic, потому что объекты строились в порядке ключей входа. (См. mono/poly/mega IC о цене megamorphic.)
Правило 2 — никогда не delete; присваивай null
delete obj.x не может просто очистить слот — он вытесняет объект из раскладки с фиксированными смещениями в словарный режим, представление на хеш-таблице, где каждый доступ к свойству идёт медленным общим путём (~50–100 циклов против 1), и объект уже не восстановится. Память, которую вы «экономите», крошечная; замедление постоянно. Присвойте полю null вместо этого — и форма останется целой.
Правило 3 — держи массив в одном element kind
V8 отслеживает element kind каждого массива и только расширяет его (в одну сторону): PACKED_SMI_ELEMENTS (самый быстрый) → PACKED_DOUBLE_ELEMENTS → PACKED_ELEMENTS (любой тип) → варианты HOLEY_* (добавляют проверку дырки на каждом доступе). Подмешивание 1.5 в массив целых расширяет его навсегда; запись за length или new Array(n) делает его HOLEY. Привычки, держащие его узким:
- Предпочитайте
[]+pushвместоnew Array(n)или присваивания за длину (оба создают HOLEY). - Предпочитайте
Array.from({length}, fn)вместоnew Array(n).fill(), когда нужен PACKED-массив заданного размера. - Не храните смешанные типы (Smi, double, object) в одном горячем массиве; разделяйте по типу или используйте типизированный массив.
Правило 4 — корми горячие функции аргументами согласованной формы
Inline cache функции ключуются по формам и типам её аргументов. Вызывайте render(node) с одной формой узла — и IC остаются monomorphic; вызывайте с пятью разными формами — они становятся megamorphic, и TurboFan не может специализироваться. Если горячая функция обязана принимать варианты, разбейте её на функции под конкретные формы или нормализуйте входы к одной форме до горячего цикла.
Правило 5 — держи числа в диапазоне Smi (или иди в типизированные)
Малое целое (Smi, ±2^31 на 64-битах) живёт inline в слоте указателя с арифметикой в одной инструкции; пересеките эту границу хоть раз — и значение станет аллоцированным в куче HeapNumber, а цикл TurboFan, предполагавший Smi, деоптится с reason: Smi. Ограничьте счётчики и id диапазоном Smi или сразу заложитесь на double через Float64Array, чтобы оптимизатор специализировался на double и никогда не откатывался. (См. Smi и double о границе Smi/HeapNumber.)
Правило 6 — держи диспетчеризацию стабильной
Точка вызова, вызывающая одну и ту же функцию (или одну из немногих стабильных), оптимизируется хорошо. Точка, вызывающая много разных функций — гигантский switch, диспетчеризующий к десяткам обработчиков, или метод, вызываемый полиморфно по многим подклассам, — становится megamorphic и теряет инлайнинг. Предпочитайте малый стабильный набор callee на горячих путях; Map типизированных обработчиков, построенный однажды, лучше пересоздания замыканий на каждый вызов.
- Monomorphic загрузка свойства
- ~1 цикл
- Megamorphic общая загрузка
- ~в 10-50x медленнее
- Доступ в словарном режиме (после delete)
- ~50-100 циклов, навсегда
- Арифметика Smi
- 1 инструкция, без аллокации
- Арифметика HeapNumber
- аллок + разыменование + риск деопта
- Доступ к HOLEY-массиву
- +1 проверка дырки на доступ
Нужно убрать поле из горячего долгоживущего объекта. Что делаете?
Горячий числовой массив стартует как PACKED_SMI. Какая одна операция деградирует его наиболее устойчиво?
Расставьте эти привычки работы с объектами от самой дружелюбной к мономорфизму к самой разрушительной.
- 1 Литерал объекта со всеми полями в фиксированном порядке
- 2 Конструктор класса, присваивающий каждое поле безусловно
- 3 Добавление некоторых полей условно после создания
- 4 delete на свойстве (вынуждает постоянный словарный режим)
▸Ещё практика
Практический аудит: возьмите одну горячую фабрику или конструктор и прогоните её сборку под --trace-ic (или смоделируйте как песочница в этом юните — посчитайте число различных сигнатур Object.keys). Если вход с фиксированной схемой даёт более одной сигнатуры формы, условное или позднее поле протекает вторым hidden class. Исправьте, инициализируя каждое поле безусловно, затем перетрассируйте и подтвердите, что точка доступа сообщает monomorphic.
- 01Почему инициализация каждого поля безусловно (пусть и в null) лучше условного добавления полей?
- 02Разберите лестницу element kind и привычки, держащие массив быстрым.
- 03Что делает точку вызова megamorphic и как держать диспетчеризацию стабильной?
Дисциплина мономорфизма — это обратная сторона каждого провала в этом треке: вместо того чтобы удивлять спекулятивный оптимизатор V8, вы держите его ставки выигрышными. Шесть правил, каждое привязано к механизму. Стройте объекты со всеми полями в фиксированном порядке через class или фабрику, чтобы каждый экземпляр разделял один hidden class (фикс для отсутствующих данных — null, а не пропуск). Никогда не delete — он вынуждает постоянный словарный режим; присваивайте null. Держите каждый горячий массив в одном element kind (предпочитайте [] + push и Array.from вместо new Array(n); никогда не смешивайте Smi/double/object и не создавайте дырки). Кормите горячие функции аргументами согласованной формы, чтобы их IC оставались monomorphic. Держите числовые значения в диапазоне Smi или заложитесь на double через типизированный массив, чтобы избежать деоптов HeapNumber. И держите диспетчеризацию к малому стабильному набору callee, чтобы точки вызова не стали megamorphic. Ни одно из этого не микротрюк; вместе это то, как пишут JavaScript, остающийся на быстром пути по умолчанию. Теперь, когда потянетесь к delete, увидите условное поле или смешаете типы в горячем массиве — вы узнаете это как ставку против оптимизатора и будете знать дешёвый фикс.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.