Архитектура V8 - от кода до машинных инструкций
V8 — это высокопроизводительный JavaScript движок от Google, используемый в Chrome и Node.js. Понимание его архитектуры критически важно для написания оптимизированного кода.
Pipeline V8: трёхуровневая компиляция
Parser → AST
function add(a, b) {
return a + b;
}
AST (Abstract Syntax Tree):
{
"type": "FunctionDeclaration",
"id": { "name": "add" },
"params": [
{ "name": "a" },
{ "name": "b" }
],
"body": {
"type": "ReturnStatement",
"argument": {
"type": "BinaryExpression",
"operator": "+",
"left": { "name": "a" },
"right": { "name": "b" }
}
}
}
Ignition Interpreter
Ignition преобразует AST в bytecode и начинает выполнение:
// Bytecode для add(a, b)
Ldar a0 // Load argument 0 (a) в аккумулятор
Add a1, [0] // Добавить argument 1 (b)
Return // Вернуть результат
Зачем bytecode?
- Быстрый старт (не нужна компиляция)
- Экономия памяти (компактнее AST)
- Легко деоптимизировать обратно
Sparkplug - Baseline Compiler
Sparkplug (добавлен в V8 9.1) компилирует часто вызываемый bytecode в неоптимизированный машинный код:
Bytecode → Machine Code (1:1 mapping)
Преимущества:
- Быстрее интерпретатора (~2x)
- Без затрат на профилирование
- Промежуточный уровень перед TurboFan
TurboFan - Optimizing Compiler
TurboFan создаёт высокооптимизированный машинный код на основе feedback:
function add(a, b) {
return a + b;
}
// После 1000+ вызовов с числами
add(1, 2); // V8 замечает: всегда числа!
add(5, 10);
add(100, 200);
// TurboFan оптимизирует: предполагая числа
// Генерирует машинный код для чисел напрямую
Оптимизации TurboFan:
- Type specialization
- Inline caching
- Function inlining
- Loop unrolling
- Dead code elimination
Optimization и Deoptimization
Когда происходит оптимизация?
function calculate(x) {
return x * 2;
}
// Вызов 1-100: Ignition (bytecode)
for (let i = 0; i < 100; i++) calculate(i);
// Вызов 100+: Sparkplug (baseline)
// V8: "Эта функция горячая, скомпилируем в baseline"
// Вызов 1000+: TurboFan (optimized)
// V8: "Всегда числа! Оптимизируем под числа"
Deoptimization (откат оптимизации)
function calculate(x) {
return x * 2;
}
// V8 оптимизировал под числа
for (let i = 0; i < 10000; i++) {
calculate(i); // Числа - оптимизированный код
}
// Неожиданный тип!
calculate("hello"); // Строка - DEOPT!
// V8 откатывается к bytecode и заново собирает feedback
Стоимость deopt:
- Откат к bytecode (медленно)
- Потеря оптимизированного кода
- Заново сбор статистики
Hidden Classes (Shapes/Maps)
V8 оптимизирует доступ к свойствам объектов через Hidden Classes:
class Point {
constructor(x, y) {
this.x = x; // Hidden Class C0 → C1
this.y = y; // Hidden Class C1 → C2
}
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);
// p1 и p2 имеют одинаковый Hidden Class C2
Hidden Class хранит:
- Список свойств и их offsets
- Типы значений
- Transitions (переходы при добавлении свойств)
Убийцы производительности
// Плохо: разные Hidden Classes
const p1 = { x: 1, y: 2 };
const p2 = { y: 2, x: 1 }; // Другой порядок!
// Плохо: dynamic properties
const p3 = { x: 1, y: 2 };
p3.z = 3; // Новый Hidden Class!
// Хорошо: одинаковая структура
const p4 = { x: 1, y: 2 };
const p5 = { x: 3, y: 4 }; // Тот же Hidden Class
Inline Caching (IC)
Inline Cache ускоряет доступ к свойствам, кешируя их location:
function getX(point) {
return point.x;
}
// Первый вызов
getX({ x: 1, y: 2 });
// V8: "x находится по offset 0 для Hidden Class C2"
// Последующие вызовы
getX({ x: 3, y: 4 });
// V8: "Тот же Hidden Class! Используем кеш - offset 0"
Типы IC:
- Monomorphic (лучший случай):
// Всегда один Hidden Class
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4 });
- Polymorphic (2-4 разных класса):
getX({ x: 1, y: 2 });
getX({ x: 1, y: 2, z: 3 }); // Другой Hidden Class
- Megamorphic (5+ классов - медленно!):
// Плохо: слишком много разных структур
for (let i = 0; i < 10; i++) {
getX({ x: 1, [i]: i }); // Каждый раз новый Hidden Class!
}
Memory Management: Heap Organization
Generational Garbage Collection
Идея: Большинство объектов умирают молодыми.
Scavenge GC (Young Generation):
// Создаём временные объекты
function process() {
const temp = { data: new Array(1000) }; // Умрёт сразу
return temp.data.length;
}
// temp уничтожается Scavenge GC (~1-2ms)
Mark-Sweep-Compact (Old Generation):
// Долгоживущие объекты
const cache = new Map(); // Выживает Scavenge → Old Gen
cache.set('key', largeData);
// Удаляется Major GC (~50-100ms)
Performance Best Practices
Избегайте deoptimization
Не меняйте типы аргументов функций. Используйте TypeScript для контроля типов.
Сохраняйте форму объектов
Инициализируйте все свойства в конструкторе. Не добавляйте свойства динамически.
Monomorphic > Polymorphic > Megamorphic
Функции должны работать с объектами одинаковой структуры (Hidden Class).
Избегайте delete
delete obj.prop создаёт новый Hidden Class. Используйте obj.prop = undefined.
Примеры оптимизаций
// Плохо: разные типы
function add(a, b) {
return a + b;
}
add(1, 2); // Числа
add("a", "b"); // Строки - DEOPT!
// Хорошо: один тип
function addNumbers(a, b) {
return a + b;
}
function addStrings(a, b) {
return a + b;
}
// Плохо: dynamic properties
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
const p = new Point(1, 2);
p.z = 3; // Новый Hidden Class!
// Хорошо: все свойства в конструкторе
class Point3D {
constructor(x, y, z = 0) {
this.x = x;
this.y = y;
this.z = z; // Всегда одинаковая структура
}
}
Инструменты для анализа
Chrome DevTools
// Проверка оптимизации функции
%OptimizeFunctionOnNextCall(myFunction); // --allow-natives-syntax
myFunction();
%GetOptimizationStatus(myFunction);
Node.js
# Запуск с флагами V8
node --trace-opt --trace-deopt app.js
# Вывод:
# [optimizing: myFunction / 0x...]
# [bailout: myFunction - type mismatch]
Итог:
V8 использует многоуровневую компиляцию (Ignition → Sparkplug → TurboFan) для баланса между скоростью старта и производительностью. Понимание Hidden Classes, Inline Caching и условий деоптимизации помогает писать код, который V8 может эффективно оптимизировать.