open atlas
↑ К треку
Внутренности движка JavaScript JSE · 08 · 01

Трассировка оптимизации и деопта

Инструментарий практика для наблюдения, как V8 оптимизирует и откатывается: --trace-opt, --trace-deopt, --trace-ic, --print-bytecode, --allow-natives-syntax (%OptimizeFunctionOnNextCall, %GetOptimizationStatus) и node --prof / --cpu-prof.

JSE Senior ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

Функция на пути рендера быстра 200 мс, затем еле ползёт. CPU-профиль (flame graph — диаграмма горячих стеков во времени) показывает, что она каждые несколько секунд снова всплывает на вершину. Вы подозреваете цикл деопта — но подозрение это не диагноз. V8 точно скажет, что случилось, на каком смещении байт-кода и почему, если спросить его правильными флагами. Этот урок — инструментарий для такого вопроса. К концу вы будете знать, какой флаг взять первым, как прочитать строку деопта до названной причины и как управлять оптимизатором вручную в контролируемом эксперименте.

Мышление: воспроизвести, трассировать, прочитать, исправить

Работа над производительностью в V8 — это не гадание. Движок инструментирован изнутри, и Node/d8 раскрывают эту инструментацию через флаги. Цикл всегда один и тот же: детерминированно воспроизвести медленный путь, запустить его под нужным флагом трассировки, прочитать события, которые флаг выпускает, затем исправить данные или поток управления, вызвавшие их — но не движок.

Эти флаги работают с node (он встраивает V8) и с d8 (отдельная оболочка V8). Для разовых экспериментов d8 чище, потому что в нём нет шума Node. Для вопроса «что делает моё настоящее приложение» нужен node --prof или --cpu-prof.

Трассировка ярусов: —trace-opt и —trace-deopt

--trace-opt печатает строку каждый раз, когда функция выбрана для оптимизации, и каким ярусом компилятора. Вы узнаёте, когда функция стала достаточно горячей, чтобы скомпилироваться, и дошла ли она до Maglev или TurboFan:

$ node --trace-opt app.js
[marking 0x... <JSFunction parseRow> for optimization to MAGLEV, ...]
[completed optimizing 0x... <JSFunction parseRow> (target MAGLEV)]
[marking 0x... <JSFunction parseRow> for optimization to TURBOFAN, ...]

--trace-deopt — флаг, к которому тянешься чаще всего. Он печатает строку каждый раз, когда оптимизированная функция откатывается обратно в интерпретатор — событие, стоящее почти за каждой регрессией «быстро, потом медленно»:

[deoptimizing (DEOPT eager): begin 0x... <JSFunction sum> (opt #3) @5, FP to SP ...
 ;;; deoptimize at <app.js:12:14>, reason: Smi, ...

Читайте слева направо: вид (eager — guard упал на входе в проверяемую операцию; lazy — функцию инвалидировали, пока она висела приостановленной на стеке; soft — откат из-за бедности обратной связи, который не выбрасывает код), функция и id оптимизации, смещение байт-кода (@5), место в исходнике и причина — здесь Smi, означающая, что значение, которое оптимизированный код считал малым целым, им не было. Это одно слово и есть диагноз: число вышло за диапазон Smi или пришло не целое.

Набор флагов практика
--trace-opt
когда + каким ярусом оптимизировано
--trace-deopt
вид деопта, смещение, причина
--trace-ic
переходы состояния IC по точкам
--print-bytecode
дамп байт-кода Ignition
--print-opt-code --code-comments
маш. код TurboFan
node --prof / --cpu-prof
сэмпл. / DevTools профиль

Трассировка IC и байт-кода

--trace-ic логирует каждый переход состояния inline cache в каждой точке доступа к свойству и вызова: продвижение от неинициализированного (0) к premonomorphic, monomorphic (1), polymorphic (P) и megamorphic (N). Когда горячая точка, которую вы ждали monomorphic, показывает N, вы нашли расхождение форм — тема юнита 03. В логе есть указатели map (hidden class), которые она видела, так что можно посчитать число различных форм, попавших в точку.

--print-bytecode выгружает байт-код Ignition для каждой скомпилированной функции — полезно подтвердить, что интерпретатор реально исполняет (например, что загрузка свойства стала общим LdaNamedProperty, а не чем-то дешевле). --print-opt-code с --code-comments выгружает сгенерированный TurboFan машинный код с аннотациями, привязывающими инструкции к исходнику; к нему тянутся только когда нужно подтвердить предположение на уровне инструкций, не для повседневной работы.

Принуждаем к делу через —allow-natives-syntax

Для детерминированных экспериментов --allow-natives-syntax разблокирует интринзики с префиксом %, чтобы можно было управлять оптимизатором вручную, не дожидаясь естественного разогрева функции:

function sum(a, b) { return a + b; }

sum(1, 2);                         // разогреть обратную связь (движок увидел Smi)
%OptimizeFunctionOnNextCall(sum);  // запросить оптимизацию
sum(3, 4);                         // этот вызов исполняется оптимизированным кодом
console.log(%GetOptimizationStatus(sum));

%GetOptimizationStatus(fn) возвращает битовую маску, которую декодируешь бит за битом. Биты, которые важны: оптимизирована, turbofanned, maglevved, интерпретируется, помечена на деоптимизацию, является функцией и никогда не оптимизировать (функция содержит конструкцию, которую V8 отказывается оптимизировать). Значение со взведёнными битами «оптимизирована» и «turbofanned» означает, что исполняется код TurboFan; если взведён бит «помечена на деоптимизацию», следующий вызов откатится. Прочие интринзики дополняют набор: %HasFastProperties(obj) говорит, остаётся ли объект в быстром (не словарном) режиме, а %DebugPrint(obj) выгружает его полную внутреннюю раскладку — указатель map, значения свойств и element kind массива (PACKED_SMI против HOLEY и так далее).

Профилируем настоящее приложение

Флаги трассировки отвечают на вопрос «почему медленна вот эта функция». Чтобы сначала найти, какая функция медленна, профилируйте:

  • node --prof пишет isolate-*.log из сэмплированных стеков; node --prof-process isolate-*.log превращает его в человекочитаемый отчёт с разбивкой тиков по функциям и секцией, выделяющей время Ignition против оптимизированного кода и время GC.
  • node --cpu-prof пишет .cpuprofile, который грузишь прямо в панель Performance Chrome DevTools (или VS Code) ради flame graph — читать его гораздо легче, чем текстовый профиль.

Дисциплина: сначала профиль, чтобы найти доминирующую стоимость, затем нацелить --trace-deopt / --trace-ic на функцию, которую обвинил профиль. Слепая трассировка всего приложения топит вас в строках.

Почему это работает

Чем отличается soft-деопт: eager/lazy-деопт выбрасывает оптимизированный код, и функция должна заново разогреться и перекомпилироваться. Soft-деопт случается, когда оптимизированный код попадает на путь, для которого у него нет обратной связи (причина insufficient type feedback); V8 сохраняет код и просто собирает обратную связь. Горстка soft-деоптов на старте — норма. Повторяющийся eager-деопт на той же функции в том же смещении — это патология, цикл деопта, и слово-причина говорит о сломанном предположении.

Викторина

Строка `--trace-deopt` гласит `deoptimizing (DEOPT eager) ... reason: Smi` на функции, суммирующей массив. Какова наиболее вероятная причина?

Викторина

Вы пока не представляете, какая функция горячая. Какой инструмент берёте ПЕРВЫМ?

Расставь шаги по порядку

Расставьте по порядку шаги грамотной диагностики производительности V8 с нуля.

  1. 1 Собрать детерминированное репро, прогоняющее медленный путь
  2. 2 Профилировать через node --prof / --cpu-prof, чтобы найти доминирующую функцию
  3. 3 Запустить эту функцию под --trace-deopt / --trace-ic, чтобы назвать причину
  4. 4 Исправить данные или поток управления, сломавшие предположение, затем перепроверить
Вспомните перед уходом
  1. 01
    Разберите чтение одной строки --trace-deopt. Какие у неё поля и какое из них диагноз?
  2. 02
    Как декодировать %GetOptimizationStatus и что делать с %HasFastProperties и %DebugPrint?
  3. 03
    В чём разница между node --prof и node --cpu-prof и где каждый в рабочем процессе?
Итог

Диагностика производительности V8 — это цикл из четырёх шагов: детерминированно воспроизвести, трассировать, прочитать события, исправить данные или поток управления. Флаги — это инструментарий. --trace-opt сообщает, когда функция оптимизирована и каким ярусом (Maglev/TurboFan); --trace-deopt сообщает каждый откат с его видом (eager/lazy/soft), смещением байт-кода, местом в исходнике и словом-причиной — и эта причина (Smi, wrong map, lost precision) и есть диагноз. --trace-ic раскрывает переходы состояния inline cache, чтобы заметить расхождение форм; --print-bytecode и --print-opt-code --code-comments выгружают байт-код Ignition и машинный код TurboFan для подтверждения. Под --allow-natives-syntax управляешь оптимизатором вручную через %OptimizeFunctionOnNextCall, декодируешь битовую маску %GetOptimizationStatus и осматриваешь раскладку через %HasFastProperties и %DebugPrint. Чтобы найти, какую функцию трассировать, сначала профилируй через node --prof (затем --prof-process) или --cpu-prof ради flame graph в DevTools. Профиль чтобы локализовать, трассировка чтобы диагностировать, фикс выше движка. Теперь, когда увидишь картину «быстро — потом медленно» на одной и той же функции, знаешь первый ход: --cpu-prof, найти виновника, затем --trace-deopt — слово-причина назовёт сломанное предположение.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено
Связанные уроки
встречается в3

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.