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

Спекуляция и guards

TurboFan запекает оптимистичные предположения из обратной связи в быстрый код, защищённый дешёвыми guards — CheckMaps, CheckSmi, CheckBounds, CheckString. Как guards охраняют быстрый путь

JSE Senior ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Самый быстрый способ прочитать point.x — это одна машинная инструкция: загрузить значение по фиксированному байтовому смещению от указателя на объект. Но это корректно, только если point действительно имеет ту форму, которую TurboFan предположил при компиляции кода. Поэтому TurboFan делает нечто дерзкое — он выпускает ту самую одноинструкционную загрузку, а перед ней ставит единственное дешёвое сравнение: «у этого объекта всё ещё та карта, которую я ожидаю?» Если да — быстрая загрузка исполняется. Если нет — вся функция откатывается. Это сравнение и есть guard, и всё искусство спекулятивной оптимизации — делать guards, которые почти никогда не падают.

Оптимизм под защитой

Предыдущие уроки задали входы: обратная связь говорит «этот сайт всегда видел карту M и Smi-операнды», и TurboFan может на этом специализироваться. Но «всегда видел» — утверждение о прошлом. JavaScript волен передать функции что-то иное при следующем вызове. Спекулятивная оптимизация разрешает это напряжение простым контрактом: предположи наблюдённые типы, выпусти код, валидный только под этим предположением, и защити каждое предположение guard, который дёшево проверяет его перед исполнением быстрого кода.

Guard — это крошечная последовательность инструкций, которая тестирует предположение и при провале передаёт управление целиком вон из оптимизированного кода (деопт — урок 05). Ключевое свойство — асимметрия: когда guard проходит — подавляюще частый случай для стабильного кода, — он стоит почти ничего (сравнение и непринятая ветка), а код за ним максимально быстр, ведь может предполагать тип без дальнейших проверок. Цена корректности платится только на редком провальном пути.

Guards, которые вы увидите в причинах --trace-deopt:

  • CheckMaps — «у этого объекта всё ещё hidden class M». Охраняет загрузку/сохранение свойства по фиксированному смещению. Причина провала: wrong map.
  • CheckSmi / CheckNumber — «это значение — малое целое» / «это число». Охраняет целочисленную или float64-арифметику без проверок тегов и боксинга. Причина провала: not a Smi, not a heap number.
  • CheckBounds — «этот индекс в пределах длины массива». Охраняет сырую загрузку элемента без перепроверки границ внутри цикла. Причина провала: out of bounds.
  • CheckString, CheckInternalizedString, CheckHeapObject и им подобные — аналогичные guards для строковых и указательных предположений.

Инлайнинг: сплавление guards и тел

Задайте себе вопрос: если оптимизатор не видит сквозь границу вызова, что происходит с guard, который вызывающий уже доказал, а вызываемый дублирует? Без инлайнинга вы платите за него дважды. Вызов функции — это барьер: оптимизатор не видит сквозь него, а сам вызов стоит настройки фрейма. Инлайнинг (встраивание вызываемого в граф вызывающего) убирает барьер для небольших горячих вызываемых, чья обратная связь называет известную цель. TurboFan вплетает граф вызываемого в вызывающего, и теперь guards вызываемого и вызывающего живут в одной области. Выигрыши накапливаются:

  • CheckMaps, который вызывающий уже доказал, не повторяется внутри встроенного тела (устранение избыточности через границу инлайна).
  • Чистое вычисление вызываемого можно планировать вместе с вызывающим, выносить из общих циклов, объединять как общие подвыражения.
  • Escape-анализ (далее) теперь может увидеть, что объект, который вызываемый аллоцирует и возвращает, на самом деле никогда не покидает объединённую функцию.

Полиморфный инлайнинг обрабатывает до ~4 известных целей с предварительной проверкой карты, диспетчеризующей между встроенными телами; сверх этого сайт мегаморфный, и вызов остаётся обобщённым, не встроенным.

Escape-анализ: удаление аллокации

Это оптимизация, которая удивляет людей больше всего, ведь она делает объекты бесплатными. Escape-анализ спрашивает о каждой аллокации: покидает ли функцию хоть какая-то ссылка на этот объект — сохранена в поле, переживающее вызов, передана вызываемому, который мог бы её удержать, возвращена? Если ответ нет — объект чисто локальный блокнот, — TurboFan выполняет скалярную замену (scalar replacement): удаляет аллокацию целиком и заменяет поля объекта обычными SSA-значениями, живущими в регистрах.

Рассмотрим горячее вычисление расстояния, строящее временную точку:

function dist(ax, ay, bx, by) {
  const d = { x: ax - bx, y: ay - by }; // временный объект, не покидает функцию
  return Math.sqrt(d.x * d.x + d.y * d.y);
}

Без escape-анализа каждый вызов аллоцирует d в куче, пишет два поля, читает их обратно и оставляет мусор для GC. С escape-анализом TurboFan доказывает, что d никогда не покидает dist, удаляет аллокацию и трактует d.x и d.y как два значения в регистрах. Выпущенный код имеет нулевую аллокацию в куче, без записей полей, без чтений полей и не создаёт давления на GC — объект существовал только в исходном тексте, а не в работающем машинном коде. Это огромно для временных точек, итераторов, мешков опций и промежуточных при деструктуризации, которыми пронизан реальный код: написаны для ясности, скомпилированы в ничто.

Guards и оптимизации, которые они разблокируют
Цена CheckMaps (проход)
1 сравнение + ветка
Быстрая загрузка свойства
1 mov по фикс. смещению
CheckSmi охраняет
нетегированную арифметику
CheckBounds охраняет
сырую загрузку, без перепроверки
Предел полиморф. инлайна
~4 цели
Результат escape-анализа
0 аллок. для непокид. объекта
Скалярно заменённые поля
живут в регистрах
Путь провала
деопт в интерпретатор
Викторина

Почему guard CheckMaps достаточно дёшев, чтобы ставить его перед каждым оптимизированным доступом к свойству?

Викторина

Какую версию временного объекта может устранить escape-анализ?

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

Расставьте, что делает оптимизированный код при спекулятивном мономорфном чтении свойства.

  1. 1 Загрузить указатель карты объекта из его заголовка
  2. 2 CheckMaps: сравнить его с ожидаемой картой M
  3. 3 Карта совпадает: взять быстрый путь
  4. 4 Загрузить свойство одной инструкцией по предвычисленному смещению
Почему это работает

Зачем охранять, а не выводить тип заново каждый раз? Потому что повторный вывод — полная диспетчеризация по типу на каждой операции — это ровно обобщённое поведение интерпретатора, ради ухода от которого TurboFan и существует. Guard — это ставка: заплати одну дешёвую проверку вперёд, затем исполни длинный отрезок кода, который предполагает ответ и не нуждается в дальнейших проверках. Для стабильного мономорфного кода ставка окупается практически на каждом вызове, и амортизированная цена корректности округляется до нуля. Ставка прогорает, только когда предположение нарушается раз за разом — это цикл деоптимизации урока 05.

Вспомните перед уходом
  1. 01
    Что такое guard, назовите частые и почему дизайн асимметричен?
  2. 02
    Как инлайнинг взаимодействует с guards и escape-анализом?
  3. 03
    Объясните escape-анализ и скалярную замену на примере и укажите его предел.
Итог

Спекулятивная оптимизация — это то, как TurboFan превращает прошлые наблюдения в быстрый код, не жертвуя корректностью. Он предполагает типы, записанные feedback vector, выпускает машинный код, валидный только под этими предположениями, и защищает каждое предположение дешёвым guard: CheckMaps для формы объекта, CheckSmi/CheckNumber для числового типа, CheckBounds для индексов массива, плюс строковые и указательные guards. Дизайн намеренно асимметричен — проходящий guard это одно сравнение и непринятая ветка, так что быстрый путь за ним (одноинструкционная загрузка по смещению, нетегированная арифметика, сырое чтение элемента) максимально быстр, а реальная цена платится лишь на редком провальном пути, который деоптится в интерпретатор. Инлайнинг сплавляет небольших горячих вызываемых с известной целью в вызывающего, так что их guards и тела делят область, включая межграничное устранение избыточности, планирование и — критически — escape-анализ. Escape-анализ (анализ «покидает ли объект функцию») выявляет аллокации, которые никогда не покидают функцию, и скалярно заменяет их: аллокация удаляется, а её поля становятся регистрами, так что временные точки, итераторы и мешки опций компилируются в нулевую аллокацию и нулевое давление на GC. Единственная оговорка: escape-анализ помогает лишь объектам, доказуемо остающимся локальными; всё, что возвращено, сохранено или отдано непрозрачному вызываемому, покидает функцию и аллоцируется в куче как обычно. Теперь, когда вы пишете тесный внутренний цикл, строящий временный объект {x, y} на каждой итерации, вы знаете, реальная ли эта аллокация или бесплатная — и что проверить, если давление на GC вас удивит.

Практика

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

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.