Компиляторы
V8: JavaScript от парсинга до машинного кода
В 2008 году Google выпустил Chrome с V8 - и JavaScript стал в 10x быстрее за один год. До V8: JS выполнялся только в интерпретаторе. V8 придумал скомпилировать динамический язык с неизвестными типами в нативный код через спекулятивные предположения. Это изменило веб-разработку навсегда и привело к Node.js.
- **Node.js** - V8 вне браузера. Тот же Ignition + TurboFan + Hidden Classes делают server-side JavaScript конкурентоспособным с Java и Go для I/O-bound задач. npm registry (миллионы пакетов) работает на Node.js.
- **Deno** и **Bun** - альтернативы Node.js на V8 (Deno) и JavaScriptCore (Bun). Bun использует более агрессивный JIT JavaScriptCore (Apple's JS engine) и быстрее V8 на некоторых workloads.
- **V8 Turboshaft** (2023) - переписывание TurboFan на более традиционный CFG + SSA IR. Sea of Nodes оказался слишком сложным для поддержки. Turboshaft проще понять, добавлять оптимизации, и уже показывает 5-10% ускорение на некоторых benchmark.
Ignition: Bytecode Interpreter
Ignition - bytecode interpreter в V8 (с 2016). Парсит JS -> AST -> Bytecode, затем интерпретирует. Register-based VM с аккумулятором. Ignition собирает профиль: типы аргументов, частоту вызовов. Эта информация используется TurboFan для JIT оптимизации.
До Ignition (2016) V8 компилировал JS напрямую в нативный код через Crankshaft - без промежуточного байткода. Это создавало проблемы: Crankshaft не поддерживал некоторые конструкции ES6 (try/catch, generators). Ignition разделил concerns: быстрый старт (интерпретация) + профилирование + JIT (TurboFan). Ignition bytecode занимает в 2-3x меньше памяти чем нативный код Crankshaft.
Для чего Ignition собирает feedback во время интерпретации?
TurboFan: Optimizing JIT
TurboFan - оптимизирующий JIT компилятор V8 (с 2017, полностью заменил Crankshaft). Использует Sea of Nodes IR (объединённый граф данных и управления), агрессивные оптимизации, и feedback из Ignition для спекулятивной оптимизации.
Sea of Nodes - IR где control flow и data flow объединены в одном графе. Предложен Cliff Click (Java HotSpot C2, 1995). V8 TurboFan унаследовал эту идею. Преимущество: глобальные оптимизации без отдельного CFG анализа - узел просто 'плавает' в графе, планировщик размещает его оптимально. Недостаток: сложнее отлаживать и понимать. Поэтому V8 рассматривает замену TurboFan на Turboshaft - более традиционный CFG+IR подход.
Что такое deoptimization в V8 TurboFan?
Hidden Classes (Maps)
Hidden class (или Map в V8 терминологии) - внутренняя структура, описывающая 'форму' JavaScript объекта: порядок свойств и их типы. Объекты с одинаковой последовательностью добавления свойств разделяют один hidden class. Это позволяет JIT обращаться к полям через фиксированные смещения.
V8 хранит hidden classes в Heap как специальные Map объекты. Каждый JS объект содержит указатель на свой Map. Переход между Maps создаётся лениво: если много объектов проходят один путь создания, переходы эффективно кешируются. SpiderMonkey (Firefox) называет это 'Shapes', JavaScriptCore (Safari/WebKit) - 'Structures'. Концепция та же - уникальная идея для оптимизации динамических объектов, предложенная Lars Bak в Self VM (1991).
Почему `delete obj.prop` является антипаттерном с точки зрения V8 оптимизации?
Inline Caches (ICs)
Inline Cache (IC) - механизм кеширования результатов property lookup в месте вызова. Вместо поиска по цепочке прототипов при каждом `obj.x`, IC кеширует: 'для объектов с Map M1, смещение поля x = 8 байт'. При следующем вызове - прямой доступ по смещению.
IC впервые реализованы в Smalltalk-80 (Deutsch & Schiffman, 1984) - тогда называлось 'inline caching of method lookup'. Lars Bak (создатель V8) работал над Self VM в 90-х, где IC были доведены до совершенства. SpiderMonkey, JavaScriptCore, Chakra (IE Edge) - все используют IC. Современные IC в V8 называются 'feedback vector slots' - структурированное хранилище для всей информации о типах на каждом call site.
Что происходит когда IC переходит в MEGAMORPHIC состояние?
Итоги
- **Ignition** - register-based bytecode interpreter с аккумулятором. Собирает feedback о типах для TurboFan. Быстрый старт и малое потребление памяти.
- **TurboFan** - оптимизирующий JIT на Sea of Nodes IR. Спекулятивно компилирует на основе feedback. Deoptimization при нарушении предположений.
- **Hidden classes (Maps)** позволяют V8 обращаться к полям по фиксированным смещениям. Объекты с одним порядком инициализации разделяют Map. `delete` разрушает Map.
- **Inline Caches** кешируют property access по типу объекта. Monomorphic - быстро. Polymorphic - терпимо. Megamorphic - медленный fallback.
Связанные темы
V8 объединяет все концепции компилятора для динамического языка:
- JVM: архитектура — JVM и V8 решают схожие задачи JIT компиляции, но для статически и динамически типизированных языков
- Байткод и виртуальные машины — Ignition - конкретная реализация register-based VM с аккумулятором
- Глобальные оптимизации — TurboFan применяет те же оптимизации (inlining, LICM, escape analysis) что и статические компиляторы
Вопросы для размышления
- V8 делает спекулятивные предположения о типах и деоптимизирует при их нарушении. TypeScript дает статические типы, но они стираются в runtime. Мог бы TypeScript помочь V8 делать лучшие предположения - и почему этого не происходит?
- Hidden classes - гениальное решение для оптимизации динамических объектов. Но что происходит с Map в современных JS паттернах (spread, object destructuring, Object.assign)? Создают ли они новые hidden classes?
- V8 Turboshaft переходит от Sea of Nodes к CFG+SSA. Если Sea of Nodes лучше для оптимизаций (нет явного порядка), почему от него отказываются - и что конкретно теряется при переходе на CFG?