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:
- Строит Sea of Nodes — граф зависимостей (не линейный IR)
- Использует type feedback для спекулятивных оптимизаций
- Применяет классические оптимизации: inlining, dead code elimination, constant folding, loop unrolling
- Генерирует машинный код для целевой архитектуры (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
Деоптимизация — дорогая операция:
- V8 останавливает оптимизированный код
- Восстанавливает состояние стека для Ignition
- Продолжает исполнение в Ignition
- Обновляет type feedback
- Позже может оптимизировать снова (но уже учитывая новые типы)
Частые причины деоптимизации
// 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 при нарушении спекуляций |