Производительность и оптимизация в V8
V8 оптимизирует через JIT и hidden classes. Поведение нелогичное: лишний
tryломает оптимизацию, мономорфная функция в 100 раз быстрее полиморфной. Без замеров «оптимизация» делает только хуже.
Принципы V8
- JIT-компиляция: код сначала интерпретируется (Ignition), горячий — компилируется в машинный (TurboFan)
- Hidden classes: V8 присваивает форму объекту на основе порядка добавления полей.
{a: 1, b: 2}и{b: 2, a: 1}— разные hidden classes - Inline caches: V8 кеширует тип аргументов функции. Если функция получала только числа — компилируется под числа. Передача строки → деоптимизация
- Monomorphic/polymorphic/megamorphic: 1 тип → быстро, 2–4 → медленнее, 5+ → совсем плохо
Как правильно мерить
// ❌ console.time — грубый замер, не годится для микробенчмарков
console.time('x'); for (...) {}; console.timeEnd('x');
// ✅ process.hrtime.bigint — наносекунды
const t = process.hrtime.bigint;
for (let i = 0; i < 1_000_000; i++) work;
console.log(Number(process.hrtime.bigint - t) / 1e6, 'ms');
// ✅ perf_hooks
const { performance } = require('perf_hooks');
const t = performance.now();
// ...
const dt = performance.now() - t;
Правила V8-дружелюбного кода
// 1. Не меняй форму объекта в hot path
function bad() {
const o = { a: 1 };
o.b = 2; // hidden class сменился
o.c = 3; // ещё раз
}
function good() {
return { a: 1, b: 2, c: 3 }; // одна форма
}
// 2. Мономорфные функции — один тип аргументов
function fast(n) { return n * 2; } // только числа
fast(1); fast(2); fast(3); // ✅ компилируется в умножение int
fast('x'); // ❌ полиморфно, деоптимизация
// 3. Не try/catch вокруг hot loop — раньше ломал оптимизацию, в новых V8 норм, но всё равно overhead
// 4. Возвращай результат — без return компилятор может вырезать
function loop() {
let sum = 0;
for (let i = 0; i < 1e6; i++) sum += i;
return sum; // ✅
// если не вернуть — V8 видит мёртвый код и удаляет
}
// 5. Прогрев функции перед замером — даёт V8 шанс оптимизировать
for (let i = 0; i < 1000; i++) target(input); // warmup
const t = performance.now();
// измерение
Подводные камни
- «Кэшировать length» — бессмысленно, V8 это делает сам
- Замыкания то быстрые, то медленные — зависит от inlining; не оптимизируй без замера
Object.create(null)— объект без прототипа, быстрее{}для map-use, нет коллизий с__proto__- Бенчмарки в naive cycle обманывают: V8 видит что результат не используется → вырезает
- Прогрев влияет: первый запуск всегда медленнее в 5–10 раз
- Inline caches очищаются при изменении hidden class — массовая деопт
--allow-natives-syntax (debug)
node --allow-natives-syntax script.js
// Доступны спецфункции
%OptimizeFunctionOnNextCall(fn); // принудительная оптимизация
%GetOptimizationStatus(fn); // битовая маска статуса
%NeverOptimizeFunction(fn);
🎓 Источники
- 🎓 [Измерение производительности кода и оптимизация] · 2018-10-24 · YouTube · [Marp](../../../Documents/TimurShemsedinov/2018-10-24 — Измерение производительности кода и оптимизация в JavaScript и Node.js (sanq2X7Re8o).md)
- Тезисы: непредсказуемая производительность V8, console.time грубый, hrtime/perf_hooks точно, прогрев функции,
--allow-natives-syntax, TurboFan, OptimizeFunctionOnNextCall, инстанцирование через замыкание vs class, инлайнинг, миллион итераций — нужно стабилизировать тип; обмануть компилятор для замера через возврат результата - Цитата: «Без замеров оптимизация делает только хуже — половина общеизвестных советов мифы»
- Тезисы: непредсказуемая производительность V8, console.time грубый, hrtime/perf_hooks точно, прогрев функции,
- 🎓 [Мономорфный и полиморфный код, скрытые классы] · 2019-10-29 · YouTube
- Тезисы: монoморфные функции компилируются в специализированный код, добавление 2-го типа = деопт, формы объектов и inline cache