Learning Book

JIT-компиляция и оптимизации

Ignition: интерпретатор байткода

Ignition — первый этап исполнения кода в V8. Он компилирует AST в байткод — последовательность компактных инструкций. Каждая инструкция — один байт (opcode) + операнды.

function square(n) { return n * n }

Байткод (упрощённо):

Ldar a0           // загрузить параметр n
Mul a0, [0]       // умножить на n
Return            // вернуть результат

Ignition использует регистровую архитектуру с аккумулятором. Каждая функция получает набор виртуальных регистров (r0, r1, …) и аккумулятор для промежуточных результатов.

Type Feedback

При выполнении байткода Ignition записывает type feedback — информацию о типах, которые встречаются в каждой операции:

function add(a, b) { return a + b }

add(1, 2)       // feedback: a=SMI, b=SMI, результат=SMI
add(3, 4)       // feedback подтверждён: числа
add('x', 'y')   // feedback обновлён: теперь ещё и строки!

Type feedback хранится в Feedback Vector — массиве слотов, по одному на каждую операцию.

TurboFan: оптимизирующий компилятор

Когда функция вызывается достаточно часто, V8 передаёт её в TurboFan — оптимизирующий JIT-компилятор. TurboFan:

  1. Строит Sea of Nodes — граф зависимостей (не линейный IR)
  2. Использует type feedback для спекулятивных оптимизаций
  3. Применяет классические оптимизации: inlining, dead code elimination, constant folding, loop unrolling
  4. Генерирует машинный код для целевой архитектуры (x64, ARM, …)

Спекулятивные оптимизации

TurboFan «спекулирует» на основе type feedback:

function add(a, b) { return a + b }
// Feedback: a и b всегда SMI (целые числа)

// TurboFan генерирует:
// 1. Проверка: a — SMI? b — SMI?
// 2. Если да → машинная инструкция ADD (быстро)
// 3. Если нет → деоптимизация (откат в Ignition)

Ключевые оптимизации TurboFan:

ОптимизацияОписание
InliningТело вызываемой функции встраивается в вызывающую
Escape AnalysisОбъект, не «убегающий» из функции, размещается на стеке
Constant FoldingВычисление константных выражений на этапе компиляции
Dead Code EliminationУдаление недоступного кода
Range AnalysisОпределение диапазона числовых значений

Maglev: промежуточный tier

TurboFan генерирует отличный код, но компиляция занимает время. Maglev — промежуточный компилятор:

Ignition          Maglev              TurboFan
(байткод)    →    (быстрый JIT)   →   (оптимизированный JIT)
~0 мс compile     ~1 мс compile       ~10 мс compile
1x скорость       5-10x скорость      10-50x скорость

Maglev использует SSA-форму (Static Single Assignment) и type feedback, но не делает тяжёлых оптимизаций вроде escape analysis или полного inlining. Это компромисс: код лучше, чем байткод, компилируется быстрее, чем TurboFan.

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

Если спекуляция TurboFan оказалась неверной, происходит деоптимизация — откат к Ignition:

function add(a, b) { return a + b }

// Тысячи вызовов с числами → TurboFan оптимизирует для чисел
for (let i = 0; i < 10000; i++) add(i, i)

// Вдруг строка!
add('hello', ' world') // DEOPT! Откат в Ignition

Деоптимизация — дорогая операция:

  1. V8 останавливает оптимизированный код
  2. Восстанавливает состояние стека для Ignition
  3. Продолжает исполнение в Ignition
  4. Обновляет type feedback
  5. Позже может оптимизировать снова (но уже учитывая новые типы)

Частые причины деоптимизации

// 1. Смена типа аргумента
function process(x) { return x + 1 }
process(1)        // SMI
process(1.5)      // HeapNumber — деоптимизация!

// 2. Hidden class mismatch
function getX(p) { return p.x }
getX({ x: 1, y: 2 })
getX({ x: 1, z: 3 }) // другой hidden class — деоптимизация IC

// 3. Выход за пределы массива
const arr = [1, 2, 3]
arr[10] = 4 // дырка (hole) — деоптимизация array operations

// 4. Использование arguments
function bad() {
  arguments[0] = 42 // мутация arguments — блокирует оптимизации
}

OSR (On-Stack Replacement)

Что если горячий код — длинный цикл, который уже выполняется? TurboFan не может ждать следующего вызова функции. Решение — OSR: замена кода прямо в середине исполнения.

function compute() {
  let sum = 0
  for (let i = 0; i < 1_000_000; i++) { // горячий цикл
    sum += i * i
  }
  return sum
}
// V8 оптимизирует цикл через OSR, не дожидаясь завершения функции

OSR работает так: TurboFan компилирует функцию, V8 находит точку в цикле, соответствующую текущей позиции Ignition, и переключает исполнение на машинный код «на лету».

Inlining Budgets

TurboFan не инлайнит всё подряд. У каждой функции есть inlining budget — максимальный размер байткода, который TurboFan готов встроить. Маленькие функции инлайнятся легко, большие — нет.

// Инлайнится (маленькая)
const add = (a, b) => a + b

// Не инлайнится (слишком большая)
function bigFunction(data) {
  // 100 строк кода...
}

Бюджет зависит от контекста: в горячих циклах V8 более агрессивен с инлайнингом.

Итого

КонцепцияОписание
IgnitionИнтерпретатор байткода, собирает type feedback
Type FeedbackИнформация о типах в каждой операции, хранится в Feedback Vector
TurboFanОптимизирующий JIT-компилятор, спекулятивные оптимизации
MaglevБыстрый промежуточный JIT — компромисс скорости компиляции и исполнения
ДеоптимизацияОткат из машинного кода в Ignition при нарушении спекуляций