WebAssembly (Wasm) последние годы стремительно развивается, становясь ключевым элементом современного веба и обеспечивая высокую производительность для широкого спектра приложений. Одним из самых перспективных направлений его совершенствования являются спекулятивные оптимизации, которые дают возможность компиляторам генерировать машинный код, учитывая динамическую информацию о выполнении программы. В этой статье мы подробно рассмотрим две важные технологии, внедренные в движок V8 и интегрированные в Google Chrome начиная с версии M137, а именно – спекулятивный инлайнинг косвенных вызовов (call_indirect) и поддержку деоптимизаций (deopts) для WebAssembly. Обе они вместе способствуют существенному увеличению скорости выполнения Wasm, особенно в контексте WasmGC – расширения, ориентированного на поддержку управляемых языков вроде Java, Kotlin и Dart. Исторический контекст и мотивация Традиционно, высокая производительность JavaScript достигалась благодаря спекулятивным оптимизациям.
JIT-компиляторы строят предположения на основе ранее собранного динамического профиля: например, если выражение a + b неоднократно вычислялось для целочисленных значений, то генерируется машинный код, оптимизированный под целочисленное сложение. Если предположение нарушается в рантайме, происходит деоптимизация – возврат к более универсальному, но менее производительному коду, что позволяет избежать некорректности выполнения. WebAssembly в изначальной версии 1.0 был ориентирован на максимально предсказуемую, статически типизированную модель, что позволяло компилировать бинарники с минимальной необходимостью в подобных спекулятивных приемах. Главное преимущество заключалось в том, что многие оптимизации происходили еще на этапе трансляции из исходных языков, таких как C++, Rust, благодаря мощным AOT-компиляторам типа LLVM или Binaryen.
В результате рантайм-движки, включая V8, обычно обходились без сложных предположений, обеспечивая быстрое выполнение. Однако с появлением WasmGC ситуация изменилась. WasmGC добавляет поддержку богатых типов, структур, подтипов и операций с ними, делая код более подобным классическим управляемым языкам с объектно-ориентированными особенностями. Это увеличивает потенциал для динамической информации и, соответственно, для спекулятивных оптимизаций, которые могут существенно сократить накладные расходы и раскрыть новые возможности улучшения производительности. Спекулятивный инлайнинг и вызовы call_indirect Одной из важнейших оптимизаций в современных компиляторах считается инлайнинг – замена вызова функцией на саму её реализацию.
Это устраняет административные расходы на переход к подпрограмме и позволяет последующим оптимизациям работать с более широким контекстом. Однако непрямая природа вызовов call_indirect в WebAssembly, где конкретная цель вызова известна лишь во время исполнения, усложняет применение обычного инлайнинга. Спекулятивный инлайнинг решает эту проблему, опираясь на поведенческие шаблоны. Хотя call_indirect может технически направлять вызов на множество функций, в реальных сценариях часто наблюдается мономорфизм – вызовы направляются в основном к одной или нескольким функциям. Движок V8 собирает статистику вызовов в специально выделенном векторе обратной связи, фиксируя целевые функции и количество вызовов.
На основе этих данных оптимизирующий компилятор TurboFan принимает решение об инлайнинге нескольких наиболее часто вызываемых целей, ограничиваясь разумным бюджетом, чтобы не увеличивать размер и сложность сгенерированного кода чрезмерно. Процесс начинается с базового компилятора Liftoff, который генерирует код на начальном уровне и одновременно собирает статистику по каждой инструкции вызова. При достижении порога «нагретости» функции происходит переход на уровень TurboFan, который читает собранные данные и заинтересован в оптимизации call_indirect, подставляя в тело вызываемых функций оптимизированный код. При этом для безопасности выполнения создаются проверочные инструкции: если во время вызова целевая функция совпадает с предположенной, выполняется инлайнинг; в противном случае запускается деоптимизация. Эта техника значительно облегчает дальнейшие оптимизации, такие как распространение констант, устранение общих подвычислений и сведение мелких операций в более крупные.
На простых микробенчмарках на Dart прирост производительности достигает свыше 50% по сравнению с кодом без этих оптимизаций. В реальных масштабных приложениях прирост составляет от одного до восьми процентов, что весьма существенно для системного уровня. Механизм деоптимизации: уход в безопасный режим Сам по себе спекулятивный инлайнинг рождает риск ложных положительных предположений – когда во время выполнения целевая функция вызова отличается от ожидаемой. Для обработки таких случаев вводится механизм деоптимизации, позволяющий «сбросить» оптимизированный код и продолжить выполнение в базовом, универсальном режиме. Деоптимизация в V8 работает через сохранение полного состояния программы прямо в момент нарушения предположения.
Оптимизированный стек и регистры регистрируются в специальной структуре – описании кадра (FrameDescription). Затем содержимое преобразуется так, чтобы соответствовать ожиданиям базового компилятора Liftoff. После этого стек и регистры перестраиваются, и выполнение возвращается в исходный код функции на уровне интерпретируемого или базового машинного кода. Важно, что переход происходит посреди выполнения функции, когда уже произошли побочные эффекты и значения находятся в регистрах, а не в памяти. Это сложный процесс, который требует тонкой координации между механизмами компиляции, выполнения и управлением памятью.
Зато он дает возможность оптимизирующему компилятору не перегружать код обработкой всех возможных вариантов вызовов, сохраняя путь оптимального выполнения при соблюдении предположений. Результаты и перспективы Опыт внедрения спекулятивного инлайнинга и деоптимизаций в V8 показывает убедительную пользу: по набору микробенчмарков Dart наблюдается ускорение в среднем на 59% при включении обеих оптимизаций. Более того, в популярных приложениях и комплексных тестах эффект сохраняется и оказывается достаточно значительным, в некоторых случаях поднимая производительность на несколько процентов, что критично для ресурсоемких веб-приложений. Помимо повышения текущей производительности, введение деоптимизаций является фундаментом для новых оптимизаций в будущем. Например, возможна более агрессивная элиминация проверок границ массивов или интенсивное удаление лишних загрузок и сохранений данных, что особенно актуально для WasmGC, где меняется модель памяти и представления данных.
Интересной задачей остается расширение инлайнинга через языковую границу – например, инлайнинг вызовов из JavaScript в WebAssembly и обратно, что позволит еще больше сблизить две модели и ускорить гибридные приложения. Заключение Спекулятивные оптимизации WebAssembly в V8, основанные на инлайнинге косвенных вызовов и механизмах деоптимизации, являются важным шагом на пути к максимально высокопроизводительной исполняемой среде. Эти технологии позволяют динамически использовать данные профиля, создавая быстрый путь для частых сценариев и обеспечивая надежный откат при неожиданных ситуациях. В результате разработчики и пользователи получают более скоростные приложения с меньшими задержками и высоким качеством выполнения. По мере развития стандарта WebAssembly и расширения возможностей WasmGC подобные подходы будут становиться все более значимыми.
В будущем можно ожидать более глубоких интеграций и кросс-языковых оптимизаций, которые сделают WebAssembly еще более привлекательной платформой для разнообразных вычислительных задач в браузере и за его пределами.