Skip to main content
Практика завдань

Архітектура V8: від коду до машинних інструкцій

V8 — це високопродуктивний JavaScript-двигун від Google, який використовується в Chrome та Node.js. Розуміння його архітектури є критично важливим для написання оптимізованого коду.

V8 Pipeline: Три рівні компіляції

Deopt

JavaScript Джерельний код

Парсер

Абстрактне Синтаксичне дерево

Ignition Інтерпретатор

Байт-код

Виконання

Sparkplug Базовий компілятор

Машинний код неоптимізований

TurboFan Оптимізуючий компілятор

Машинний код оптимізований

Парсер → AST

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

AST (Абстрактне синтаксичне дерево):

json
{ "type": "FunctionDeclaration", "id": { "name": "add" }, "params": [ { "name": "a" }, { "name": "b" } ], "body": { "type": "ReturnStatement", "argument": { "type": "BinaryExpression", "operator": "+", "left": { "name": "a" }, "right": { "name": "b" } } } }

Інтерпретатор Ignition

Ignition перетворює AST в байт-код і починає виконання:

// Байт-код для add(a, b) Ldar a0 // Завантажити аргумент 0 (a) в акумулятор Add a1, [0] // Додати аргумент 1 (b) Return // Повернути результат

Чому байт-код?

  • Швидкий старт (компіляція не потрібна)
  • Ефективність пам'яті (більш компактний, ніж AST)
  • Легко повернути назад до байт-коду

Sparkplug - Базовий компілятор

Sparkplug (доданий у V8 9.1) компілює часто викликаний байт-код в неоптимізований машинний код:

Байт-код → Машинний код (відповідність 1:1)

Переваги:

  • Швидше, ніж інтерпретатор (~2x)
  • Без накладних витрат на профілювання
  • Проміжний рівень перед TurboFan

TurboFan - Оптимізуючий компілятор

TurboFan створює високооптимізований машинний код на основі зворотного зв'язку:

javascript
function add(a, b) { return a + b; } // Після 1000+ викликів з числами add(1, 2); // V8 помічає: завжди числа! add(5, 10); add(100, 200); // TurboFan оптимізує: припускаючи числа // Генерує машинний код для чисел безпосередньо

Оптимізації TurboFan:

  • Спеціалізація типів
  • Інлайн кешування
  • Інлайн функцій
  • Розгортання циклів
  • Видалення мертвого коду

Оптимізація та деоптимізація

Коли відбувається оптимізація?

javascript
function calculate(x) { return x * 2; } // Виклик 1-100: Ignition (байт-код) for (let i = 0; i < 100; i++) calculate(i); // Виклик 100+: Sparkplug (базовий) // V8: "Ця функція гаряча, компілювати в базовий" // Виклик 1000+: TurboFan (оптимізований) // V8: "Завжди числа! Оптимізувати для чисел"

Деоптимізація (відкат оптимізації)

javascript
function calculate(x) { return x * 2; } // V8 оптимізовано для чисел for (let i = 0; i < 10000; i++) { calculate(i); // Числа - оптимізований код } // Несподіваний тип! calculate("hello"); // Рядок - DEOPT! // V8 повертається до байт-коду і знову збирає зворотний зв'язок

Вартість деоптимізації:

  • Відкат до байт-коду (повільно)
  • Втрата оптимізованого коду
  • Повторне збирання статистики

Сховані класи (Shapes/Maps)

V8 оптимізує доступ до властивостей через Сховані класи:

javascript
class Point { constructor(x, y) { this.x = x; // Схований клас C0 → C1 this.y = y; // Схований клас C1 → C2 } } const p1 = new Point(1, 2); const p2 = new Point(3, 4); // p1 і p2 мають один і той же схований клас C2

Схований клас зберігає:

  • Список властивостей та їх зсуви
  • Типи значень
  • Перехід (зміни при додаванні властивостей)

Вбивці продуктивності

javascript
// Погано: різні сховані класи const p1 = { x: 1, y: 2 }; const p2 = { y: 2, x: 1 }; // Різний порядок! // Погано: динамічні пропси const p3 = { x: 1, y: 2 }; p3.z = 3; // Новий схований клас! // Добре: однакова структура const p4 = { x: 1, y: 2 }; const p5 = { x: 3, y: 4 }; // Один і той же схований клас

Інлайн кешування (IC)

Інлайн кеш прискорює доступ до властивостей, кешуючи їх місцезнаходження:

javascript
function getX(point) { return point.x; } // Перший виклик getX({ x: 1, y: 2 }); // V8: "x знаходиться на зсуві 0 для схованого класу C2" // Наступні виклики getX({ x: 3, y: 4 }); // V8: "Той же схований клас! Використовувати кеш - зсув 0"

Типи IC:

  1. Мономорфний (найкращий випадок):
javascript
// Завжди один схований клас getX({ x: 1, y: 2 }); getX({ x: 3, y: 4 });
  1. Поліморфний (2-4 різних класи):
javascript
getX({ x: 1, y: 2 }); getX({ x: 1, y: 2, z: 3 }); // Різний схований клас
  1. Мегаморфний (5+ класів - повільно!):
javascript
// Погано: занадто багато різних структур for (let i = 0; i < 10; i++) { getX({ x: 1, [i]: i }); // Новий схований клас щоразу! }

Управління пам'яттю: Організація купи

V8 Heap

Молода генерація Новий простір 1-8 МБ

Стара генерація Старий простір ~100 МБ+

From-Space Активні об'єкти

To-Space Копіювання під час GC

Старий простір вказівників Об'єкти з посиланнями

Старий простір даних Примітиви

Простір великих об'єктів Об'єкти > 8 КБ

Генераційне збори сміття

Ідея: Більшість об'єктів помирає молодими.

Scavenge GC (молода генерація):

javascript
// Створити тимчасові об'єкти function process() { const temp = { data: new Array(1000) }; // Гине відразу return temp.data.length; } // temp знищується Scavenge GC (~1-2мс)

Mark-Sweep-Compact (стара генерація):

javascript
// Довгоживучі об'єкти const cache = new Map(); // Переживає Scavenge → стара генерація cache.set('key', largeData); // Видаляється Major GC (~50-100мс)

Найкращі практики продуктивності

  1. Уникайте деоптимізації

Не змінюйте типи аргументів функції. Використовуйте TypeScript для контролю типів.

  1. Зберігайте структуру об'єкта

Ініціалізуйте всі пропси в конструкторі. Не додавайте пропси динамічно.

  1. Монорфний > Поліморфний > Мегаморфний

Функції повинні працювати з об'єктами однієї структури (схований клас).

  1. Уникайте delete

delete obj.prop створює новий схований клас. Використовуйте obj.prop = undefined.

Приклади оптимізації

javascript
// Погано: різні типи 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; } // Погано: динамічні пропси class Point { constructor(x, y) { this.x = x; this.y = y; } } const p = new Point(1, 2); p.z = 3; // Новий схований клас! // Добре: всі пропси в конструкторі class Point3D { constructor(x, y, z = 0) { this.x = x; this.y = y; this.z = z; // Завжди однакова структура } }

Інструменти аналізу

Chrome DevTools

javascript
// Перевірка оптимізації функції %OptimizeFunctionOnNextCall(myFunction); // --allow-natives-syntax myFunction(); %GetOptimizationStatus(myFunction);

Node.js

bash
# Запуск з прапорами V8 node --trace-opt --trace-deopt app.js # Вихід: # [оптимізація: myFunction / 0x...] # [відкат: myFunction - невідповідність типу]

Резюме:

V8 використовує багаторівневу компіляцію (Ignition → Sparkplug → TurboFan) для балансу між швидкістю запуску та продуктивністю. Розуміння схованих класів, інлайн кешування та умов деоптимізації допомагає писати код, який V8 може ефективно оптимізувати.

Суміжні статті

Зміст

V8 Pipeline: Три рівні компіляціїПарсер → ASTІнтерпретатор IgnitionSparkplug - Базовий компіляторTurboFan - Оптимізуючий компіляторОптимізація та деоптимізаціяКоли відбувається оптимізація?Деоптимізація (відкат оптимізації)Сховані класи (Shapes/Maps)Вбивці продуктивностіІнлайн кешування (IC)Управління пам'яттю: Організація купиГенераційне збори сміттяНайкращі практики продуктивностіПриклади оптимізаціїІнструменти аналізуChrome DevToolsNode.jsСуміжні статті

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?
Практика завдань