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

TurboFan: оптимизатор на море узлов

TurboFan представляет код как граф «море узлов» — рёбра значений, эффектов и управления — вместо CFG из операторов, что освобождает планировщик размещать вычисления поздно. Полный конвейер фаз от байт-кода и обратной связи до аллокации регистров и генерации кода.

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

Два вопроса про одну и ту же строку кода, const d = a.x - b.x, раскрывают, как мыслит TurboFan. Традиционный компилятор спрашивает: «какой оператор идёт перед этим?» TurboFan спрашивает: «от какого значения зависит a.x и есть ли хоть какой-то побочный эффект между этим местом и местом использования?» — и если ответ «никакого», он волен перенести это вычитание куда угодно, хоть из цикла, хоть мимо других операторов. Эта свобода — весь смысл IR «море узлов», и именно она позволяет TurboFan оптимизировать намного агрессивнее, чем компилятор, идущий оператор за оператором.

Почему граф, а не список операторов

Традиционный компилятор моделирует функцию как граф потока управления (CFG) из базовых блоков, каждый — фиксированная последовательность операторов. Порядок зашит с самого начала: оператор 3 идёт после оператора 2, потому что вы так написали. Чтобы перенести вычисление, такой компилятор должен доказать законность переноса относительно этого навязанного порядка.

TurboFan переворачивает это. Его IR — это граф «море узлов» (sea-of-nodes), где узел — это операция, а рёбра — не исходный порядок — кодируют каждое ограничение, которое реально важно. Понятия «порядок инструкций» нет до самого конца. Граф несёт три вида рёбер:

  • Рёбра значений (зависимости по данным): у subtract есть рёбра значений к двум загрузкам, которые он потребляет. Это говорит «мне нужны эти входы», и ничего о том, когда.
  • Рёбра эффектов (порядок наблюдаемых побочных эффектов): запись, вызов, который может изменить состояние, загрузка свойства, способная вызвать getter, — они сцеплены в цепочку эффектов, чтобы их относительный порядок сохранялся. Чистые операции (арифметика над известными числами) не несут ребра эффекта и свободно плавают.
  • Рёбра управления (ветвления, слияния, циклы): скелет из if/loop/return, решающий, какие узлы вообще исполнятся.

Поскольку у чистого вычитания есть только рёбра значений и нет ограничений по эффекту или управлению, планировщик (поздняя фаза) может разместить его где угодно, где доступны его входы — включая вынос из цикла, если его входы инвариантны относительно цикла. Это и имеет в виду mrale.ph под «планировщик размещает вычисления поздно»: у узлов нет фиксированного дома до планирования, и оно назначает самый дешёвый законный.

Конвейер фаз

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

Построение графа превращает байт-код в начальный граф «море узлов», прикрепляя наблюдения из feedback vector, чтобы поздние фазы знали спекулируемые типы. Типизация и типизированное понижение распространяют типы по графу (+ двух узлов типа SignedSmall типизируется как число в известном диапазоне) и заменяют абстрактные операции на специализированные по типу — обобщённый JSAdd становится NumberAdd, затем потенциально Int32Add.

Затем проходы оптимизации переписывают граф:

  • Инлайнинг — узел вызова, чья обратная связь называет небольшую известную цель, заменяется копией графа вызываемого, вплетая его рёбра значений и эффектов в вызывающего. Это то, что разблокирует межфункциональную оптимизацию; без него каждый вызов — непрозрачный эффект.
  • Escape-анализ со скалярной заменой — объект, чья ссылка никогда не покидает функцию (подробно в уроке 04), теряет свою аллокацию, а его поля превращаются в обычные узлы графа (регистры), устраняя аллокацию в куче и давление на GC.
  • Устранение загрузок/сохранений — загрузка, которая обязана прочитать только что сохранённое значение, заменяется самим сохранённым значением; сохранение, перезаписанное до любого чтения, удаляется.
  • Устранение избыточности и свёртка констант — CheckMaps, который уже доказан доминирующим CheckMaps, удаляется; 2 * 3 становится 6; ветка по заведомо истинному условию схлопывается.
  • Выбор представления — решает машинное представление каждого значения: тегированный указатель, 32-битное целое в регистре или 64-битный float в FP-регистре, вставляя преобразования только там, где граница представления реально пересекается.

Планирование — это фаза, которая наконец назначает узлы базовым блокам и порядок внутри них — размещая чистые узлы так поздно и так вне-циклово, как законно. Выбор инструкций сопоставляет паттерны графа машинным инструкциям целевой ISA. Аллокация регистров (TurboFan использует продвинутый аллокатор, а не линейное сканирование, как Maglev) назначает физические регистры и вставляет сбросы (spills). Генерация кода выпускает финальные байты. На выходе — блоб нативного машинного кода, установленный на функцию.

TurboFan против более лёгких ярусов
Форма IR
граф «море узлов»
Виды рёбер
значения / эффекты / управление
Аллокатор регистров
на графе (не линейное скан.)
Предел инлайнинга (полиморф.)
~4 известных цели
Время компиляции
десятки-сотни мс / функцию
Работает на
фоновом потоке
Качество Maglev против TurboFan
~50-70%
Флаг трассировки
--trace-turbo / --trace-opt

Где он работает и как его инспектировать

TurboFan компилирует на фоновом потоке (параллельная компиляция, урок 01): главный поток продолжает исполнять функцию на её текущем ярусе, а оптимизированный код подменяется, когда готов. Это источник самой частой ментальной ошибки о нём (см. ниже). Чтобы инспектировать граф, --trace-turbo дампит IR на каждой фазе в файлы, которые можно открыть в визуализаторе Turbolizer; --trace-opt логирует решения более высокого уровня. mrale.ph и посты v8.dev/blog — канонические глубокие референсы по графу и его фазам.

Викторина

В IR «море узлов» TurboFan у чистого целочисленного вычитания внутри цикла есть только рёбра значений (нет ребра эффекта или управления, привязывающего его). Что может сделать с ним планировщик?

Викторина

Почему типизированное понижение происходит до планирования и аллокации регистров, а не после?

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

Расставьте фазы TurboFan от входа до нативного кода.

  1. 1 Построить граф «море узлов» из байт-кода + обратной связи
  2. 2 Типизация и типизированное понижение (специализация абстрактных операций)
  3. 3 Проходы оптимизации (инлайнинг, escape-анализ, свёртка)
  4. 4 Выбор представления (tagged / int32 / float64)
  5. 5 Планирование (назначить узлы блокам и порядок)
  6. 6 Выбор инструкций + аллокация регистров + генерация кода
Почему это работает

Почему разделение рёбер значений, эффектов и управления так важно на практике? Потому что большая часть критичного к производительности JS — это числовая или массивная работа, где тяжёлые операции чистые: арифметика, сравнения, вычисление индексов. В CFG они приколочены к своему базовому блоку, и только вынос инвариантного кода может их сдвинуть, с консервативными предположениями об алиасинге. В графе «море узлов» у них нет дома до планирования, так что компилятор может выносить их из циклов, делать общую подвыражение-элиминацию через ветки и сворачивать избыточные с куда меньшей церемонией. Цена реальна — компилятор сложнее и труднее отлаживается, — но для специализации динамического языка она того стоит.

Вспомните перед уходом
  1. 01
    Какие три вида рёбер в IR «море узлов» TurboFan и почему это различие включает агрессивную оптимизацию?
  2. 02
    Пройдитесь по конвейеру TurboFan от байт-кода до нативного кода.
  3. 03
    Почему «TurboFan исполняет мой код» неверно и где на самом деле проявляется его цена?
Итог

TurboFan — это полный оптимизирующий компилятор V8, и его определяющий выбор — промежуточное представление «море узлов» (sea-of-nodes — граф, где узлы — операции, а рёбра — единственные реальные ограничения). Вместо графа потока управления из упорядоченных операторов он моделирует функцию как граф, чьи рёбра — значений (зависимости по данным), эффектов (порядок наблюдаемых побочных эффектов) и управления (ветвления, циклы, слияния) — кодируют единственные важные ограничения; исходный порядок не кодирует. У чистой операции есть только рёбра значений, так что у неё нет фиксированной позиции до фазы планирования, которая размещает её так поздно и так дёшево, как законно, — это и делает вынос из циклов, общую подвыражение-элиминацию и свёртку избыточности столь естественными. Конвейер идёт по порядку: построение графа из байт-кода и обратной связи, типизация и типизированное понижение, специализирующее абстрактные операции, проходы оптимизации (инлайнинг, escape-анализ со скалярной заменой, устранение загрузок/сохранений и избыточности, свёртка констант), выбор представления (tagged против int32 против float64), планирование, выбор инструкций, аллокация регистров на графовом аллокаторе и генерация кода. Вся компиляция идёт однажды на фоновом потоке, так что TurboFan никогда не исполняет ваш код и никогда не блокирует главный поток — он производит нативный код, который затем CPU исполняет напрямую. Инспектируйте через —trace-turbo и Turbolizer; глубокие референсы — mrale.ph и блог v8.dev. Теперь, когда вы видите в —trace-turbo вывод, что вычитание внутри цикла перенесено за его пределы, у вас есть ответ: нет ребра эффекта, и планировщик разместил его там, где позволил поток данных.

Практика

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

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.