🎉 Первое видео: Интервью с разработчиком из Meta

Архитектура 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:

  1. Monomorphic (лучший случай):
// Всегда один Hidden Class
getX({ x: 1, y: 2 });
getX({ x: 3, y: 4 });
  1. Polymorphic (2-4 разных класса):
getX({ x: 1, y: 2 });
getX({ x: 1, y: 2, z: 3 }); // Другой Hidden Class
  1. 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

1

Избегайте deoptimization

Не меняйте типы аргументов функций. Используйте TypeScript для контроля типов.

2

Сохраняйте форму объектов

Инициализируйте все свойства в конструкторе. Не добавляйте свойства динамически.

3

Monomorphic > Polymorphic > Megamorphic

Функции должны работать с объектами одинаковой структуры (Hidden Class).

4

Избегайте 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 может эффективно оптимизировать.

Связанные статьи