Конвейер компиляции WebAssembly
WebAssembly — это бинарный формат, который позволяет эффективно и безопасно запускать код на web из языков программирования, отличных от JavaScript. В этом документе мы подробно рассматриваем конвейер компиляции WebAssembly в V8 и объясняем, как мы используем различные компиляторы для обеспечения высокой производительности.
Liftoff
Изначально V8 не компилирует никакие функции в модуле WebAssembly. Вместо этого функции компилируются лениво с помощью базового компилятора Liftoff при первом вызове функции. Liftoff — это однопроходной компилятор, что означает, что он проходит по коду WebAssembly один раз и сразу создает машинный код для каждой инструкции WebAssembly. Однопроходные компиляторы превосходно справляются с быстрой генерацией кода, хотя могут применить лишь ограниченный набор оптимизаций. Действительно, Liftoff может компилировать код WebAssembly очень быстро, на десятки мегабайт в секунду.
После завершения компиляции Liftoff полученный машинный код регистрируется в модуле WebAssembly, так что при следующих вызовах функции этот скомпилированный код может быть использован немедленно.
TurboFan
Liftoff генерирует достаточно быстрый машинный код за короткий промежуток времени. Однако, так как он создает код отдельно для каждой инструкции WebAssembly, пространства для оптимизаций практически нет, например, для улучшения распределения регистров или применения общих оптимизаций компилятора, таких как устранение избыточных загрузок, снижение сложности или встроенные функции.
Именно поэтому часто вызываемые функции, которые выполняются часто, перекомпилируются с использованием TurboFan, оптимизирующего компилятора в V8 как для WebAssembly, так и для JavaScript. TurboFan — это многопроходной компилятор, что означает, что он создает несколько внутренних представлений скомпилированного кода перед созданием машинного кода. Эти дополнительные внутренние представления позволяют производить оптимизации и лучше распределять регистры, что приводит к значительно более быстрому коду.
V8 отслеживает, как часто вызываются функции WebAssembly. Как только функция достигает определенного порога, она считается горячей, и перекомпиляция запускается в фоновом потоке. После завершения компиляции новый код регистрируется в модуле WebAssembly, заменяя существующий код из Liftoff. Все новые вызовы этой функции затем будут использовать новый, оптимизированный код, созданный TurboFan, а не код Liftoff. Однако стоит отметить, что мы не выполняем замену кода на стеке. Это означает, что если код TurboFan становится доступен после вызова функции, выполнение вызова функции завершится с использованием кода Liftoff.
Кэширование кода
Если модуль WebAssembly был скомпилирован с использованием WebAssembly.compileStreaming
, то сгенерированный TurboFan код также будет кешироваться. Когда тот же модуль WebAssembly снова загружается с того же URL, кешированный код может быть использован немедленно без дополнительной компиляции. Дополнительная информация о кэшировании кода доступна в отдельной статье блога.
Кэширование кода запускается, когда сгенерированный код TurboFan достигает определенного порога. Это означает, что для больших модулей WebAssembly код TurboFan кешируется поэтапно, тогда как для маленьких модулей WebAssembly код TurboFan может никогда не кэшироваться. Код Liftoff не кэшируется, так как компиляция Liftoff почти так же быстра, как загрузка кода из кэша.
Отладка
Как упоминалось ранее, TurboFan применяет оптимизации, многие из которых включают перестановку кода, устранение переменных или даже пропуск целых секций кода. Это означает, что если вы хотите установить точку останова на определенной инструкции, может быть неясно, где выполнение программы должно фактически остановиться. Другими словами, код TurboFan не очень подходит для отладки. Поэтому, когда отладка начинается с открытия DevTools, весь код TurboFan заменяется на код Liftoff ("понижаемый уровень"), так как каждая инструкция WebAssembly соответствует точно одному участку машинного кода, и все локальные и глобальные переменные находятся в целости.
Профилирование
Чтобы сделать вещи еще более запутанными, в DevTools весь код будет снова повышаться (рекомпилироваться с помощью TurboFan), когда открывается вкладка Performance и нажимается кнопка "Record". Кнопка "Record" запускает профилирование производительности. Профилирование кода Liftoff не будет представительным, так как он используется только до завершения работы TurboFan и может быть значительно медленнее, чем результаты работы TurboFan, который будет работать в подавляющем большинстве случаев.
Флаги для экспериментов
Для экспериментов V8 и Chrome можно настроить так, чтобы WebAssembly-код компилировался только с помощью Liftoff или только с помощью TurboFan. Также можно поэкспериментировать с ленивой компиляцией, где функции компилируются только при их первом вызове. Следующие флаги активируют эти экспериментальные режимы:
-
Только Liftoff:
- В V8 укажите флаги
--liftoff --no-wasm-tier-up
. - В Chrome отключите уровневую компиляцию WebAssembly (
chrome://flags/#enable-webassembly-tiering
) и включите базовый компилятор WebAssembly (chrome://flags/#enable-webassembly-baseline
).
- В V8 укажите флаги
-
Только TurboFan:
- В V8 укажите флаги
--no-liftoff --no-wasm-tier-up
. - В Chrome отключите уровневую компиляцию WebAssembly (
chrome://flags/#enable-webassembly-tiering
) и отключите базовый компилятор WebAssembly (chrome://flags/#enable-webassembly-baseline
).
- В V8 укажите флаги
-
Ленивая компиляция:
- Ленивая компиляция — это режим, в котором функция компилируется только при её первом вызове. Как и в производственной конфигурации, функция сначала компилируется с помощью Liftoff (блокируя выполнение). После завершения компиляции Liftoff функция перекомпилируется с помощью TurboFan в фоновом режиме.
- В V8 укажите флаг
--wasm-lazy-compilation
. - В Chrome включите ленивую компиляцию WebAssembly (
chrome://flags/#enable-webassembly-lazy-compilation
).
Время компиляции
Существуют различные способы измерения времени компиляции Liftoff и TurboFan. В производственной конфигурации V8 время компиляции Liftoff можно измерить в JavaScript, замерив время, необходимое для выполнения new WebAssembly.Module()
, или время, за которое WebAssembly.compile()
выполняет обещание. Чтобы измерить время компиляции TurboFan, можно сделать то же самое в конфигурации с использованием только TurboFan.
Компиляцию также можно измерить более подробно в chrome://tracing/
, включив категорию v8.wasm
. Компиляция Liftoff — это время, прошедшее с начала компиляции до события wasm.BaselineFinished
, а компиляция TurboFan заканчивается на событии wasm.TopTierFinished
. Компиляция начинается на событии wasm.StartStreamingCompilation
для WebAssembly.compileStreaming()
, на событии wasm.SyncCompile
для new WebAssembly.Module()
и на событии wasm.AsyncCompile
для WebAssembly.compile()
. Компиляция Liftoff обозначается событиями wasm.BaselineCompilation
, а компиляция TurboFan — событиями wasm.TopTierCompilation
. На изображении выше показана трассировка, записанная для Google Earth, с выделенными ключевыми событиями.
Более подробные данные трассировки доступны в категории v8.wasm.detailed
, которая, среди прочей информации, предоставляет данные о времени компиляции отдельных функций.