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

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

Inline caches, состояния IC и деоптимизация

Суть Как per-call-site inline caches V8 отслеживают hidden classes, конечный автомат monomorphic/polymorphic/megamorphic, триггеры деоптимизации и FadedExample перехода hidden class, ломающего горячий IC.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 15 min

Одна строка кода — p.x — может выполниться за 1 такт CPU или за 100 тактов в зависимости от того, сколько разных форм объектов через неё прошло. Структура, принимающая это решение, называется inline cache, и её состояние — самое важное число производительности, которое DevTools по умолчанию не показывает.

Как работают inline caches

Каждый доступ к свойству в JS — полиморфная операция: объект может быть любой формы. V8 генерирует крохотный кэш для каждой точки вызова: когда строка выполняется первый раз, V8 записывает увиденный hidden class и смещение свойства для него. В следующий раз, когда та же строка выполняется с объектом того же hidden class, V8 читает свойство по записанному смещению напрямую — 5–10 тактов CPU. Никаких хеш-поисков, проверок типов, одна инструкция MOV.

Конечный автомат IC

Состояние IC живёт в слоте FeedbackVector для этой строки источника. Переходы односторонние — в сторону megamorphic:

СостояниеУвиденных hidden classesСтоимостьПримечания
uninitialized0Первое выполнение
monomorphic15–10 тактовБыстрый прямой доступ
polymorphic2–4Линейная цепочка ветокВсё ещё быстро
megamorphic5+Общий поиск, ~50–100 тактовВ 10–50 раз медленнее

Функция может иметь сотни IC-слотов. Один megamorphic-сайт не убивает всю функцию, но горячий цикл с megamorphic доступом к свойству убивает тело своего цикла.

Стоимость состояний IC
Monomorphic доступ
5–10 тактов
Порог megamorphic
≥5 форм на IC-слот
Замедление megamorphic
10–50× vs monomorphic
Размер IC-слота в FeedbackVector
16 байт (8Б класс + 8Б хендлер)
Стоимость общего поиска
50–100 тактов

Деоптимизация

Когда TurboFan оптимизирует функцию, он встраивает предположения: «этот аргумент всегда smi», «свойство z этого объекта всегда число», «этот цикл никогда не переполняет 32-битные индексы». Защита (guard) вставляется при каждом предположении. Если во время выполнения защита не срабатывает, V8 деоптимизирует: выбрасывает скомпилированный код, восстанавливает состояние интерпретатора из оптимизированного стека кадра и возобновляет выполнение в Ignition (или Sparkplug). Функция может позже подняться снова, но сам deopt стоит ~микросекунды.

Частые триггеры deopt:

  • Передача другого типа в ранее типизированный аргумент
  • Индексирование за пределами массива
  • Получение NaN в числовой операции
  • Доступ к удалённому свойству

Флаги Chrome DevTools --trace-opt --trace-deopt показывают deopt’ы. %OptimizationStatus(fn) при --allow-natives-syntax в d8 показывает текущий уровень.

Трасса одного перехода hidden class, ломающего IC

1/3
Викторина

Горячий цикл обрабатывает объекты из 6 разных конструкторов. Какое состояние IC достигает точка доступа к свойству?

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

Упорядочите шаги доступа к свойству в monomorphic IC, скомпилированном TurboFan:

  1. 1 TurboFan-скомпилированная функция получает указатель на объект в регистре
  2. 2 Загрузить указатель hidden class из заголовка объекта
  3. 3 Сравнить загруженный hidden class с ожидаемым из времени компиляции
  4. 4 Если равно: прочитать свойство по заранее вычисленному смещению — 1 MOV
  5. 5 Если не равно: деоптимизировать, откатиться на нижний уровень
  6. 6 Нижний уровень записывает новый hidden class, IC может сменить состояние
  7. 7 Функция может быть повторно оптимизирована с обновлённым feedback
Викторина

Функция была TurboFan-скомпилирована и работала за 5мкс. После рефакторинга теперь работает за 250мкс. Какова наиболее вероятная причина на уровне V8?

Почему это работает

Почему порог megamorphic равен 5, а не какому-то другому числу? Polymorphic IC хендлер V8 может встроить до 4 проверок классов с ветками прежде, чем код становится слишком большим и сложным для управления. При 5+, накладные расходы хендлера превышают пользу любой специализации, поэтому V8 откатывается к общему поиску, обрабатывающему все формы единообразно. Число 4 — константа реализации, настроенная для среднего JS-рабочего процесса; это не фундаментальный аппаратный предел.

Вспомните перед уходом
  1. 01
    Опишите конечный автомат IC: что вызывает каждый переход?
  2. 02
    Что такое защита (guard) при деоптимизации и когда она срабатывает?
  3. 03
    Точка вызова видела 3 hidden class. Как вернуть её в monomorphic?
Итог

Inline cache — это per-call-site заглушка, записывающая hidden class, который V8 видел, и смещение свойства для него. Когда тот же hidden class приходит снова, свойство читается по заранее вычисленному смещению за 5–10 тактов — никаких поисков. Конечный автомат IC переходит от uninitialized → monomorphic → polymorphic → megamorphic по мере наблюдения новых классов. Megamorphic (5+ классов) вынуждает к общему поиску стоимостью 50–100 тактов на доступ — в 10–50 раз медленнее monomorphic. Деоптимизация происходит, когда защита TurboFan не срабатывает во время выполнения; скомпилированный код выбрасывается, выполнение откатывается на Ignition. Единичный deopt дорог, но переживаем; deopt-loop — где TurboFan перекомпилирует одну функцию только чтобы deopt’нуть её на каждом вызове — катастрофически снижает пропускную способность.

Связанные уроки
встречается в143
Продолжить восхождение ↑Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записи
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.