Новый способ эффективной интеграции языков программирования с автоматическим управлением памятью в WebAssembly
Недавняя статья о WebAssembly Garbage Collection (WasmGC) на высоком уровне объясняет, как предложение по сборке мусора (GC) направлено на улучшение поддержки языков со сборкой мусора в Wasm, что крайне важно в свете их популярности. В этой статье мы углубимся в технические детали того, как такие языки, как Java, Kotlin, Dart, Python и C#, могут быть портированы в Wasm. По сути, существуют два основных подхода:
- “Традиционный” подход портирования, при котором существующая реализация языка компилируется в WasmMVP, то есть минимально жизнеспособный продукт WebAssembly, выпущенный в 2017 году.
- Подход WasmGC для портирования, при котором язык компилируется до GC-конструкций в самом Wasm, определённых в недавнем предложении о сборке мусора.
Мы объясним, что представляют собой эти два подхода, и технические компромиссы между ними, особенно в отношении размера и скорости. При этом мы увидим, что WasmGC имеет несколько существенных преимуществ, но также требует новой работы как в цепочках инструментов, так и в виртуальных машинах (VM). В следующих разделах этой статьи будет объяснено, чем команда V8 занимается в этих областях, включая демонстрационные показатели. Если вы интересуетесь Wasm, GC или и тем и другим, надеемся, эта статья будет вам интересна, обязательно ознакомьтесь с демонстрацией и ссылками на начало работы ближе к концу!
“Традиционный” подход портирования
Как языки обычно портируются на новые архитектуры? Скажем, Python хочет работать на архитектуре ARM или Dart хочет работать на архитектуре MIPS. Общая идея состоит в том, чтобы рекомпилировать виртуальную машину (VM) для этой архитектуры. Помимо этого, если у VM есть архитектурно-специфичный код, например, для компиляции в режиме JIT или AOT, то для новой архитектуры реализуется бэкенд для JIT/AOT. Этот подход логичен, потому что основная часть кода часто может быть просто рекомпилирована для каждой новой архитектуры, на которую вы портируете:
На этой схеме парсер, поддержка библиотек, сборщик мусора, оптимизатор и т.д. общие для всех архитектур в основном времени исполнения. Портирование на новую архитектуру требует лишь нового бэкенда для неё, что представляет сравнительно небольшой объём кода.
Wasm является низкоуровневой целью для компиляции, поэтому неудивительно, что традиционный подход портирования может быть использован. С момента появления Wasm мы видели, как этот подход успешно работал на практике во многих случаях, таких как Pyodide для Python и Blazor для C# (обратите внимание, что Blazor поддерживает как AOT, так и JIT компиляцию, так что это отличный пример всего вышеперечисленного). Во всех этих случаях среда выполнения языка компилируется в WasmMVP точно так же, как и любая другая программа, компилируемая в Wasm, и результат использует линейную память, таблицы, функции и другие элементы WasmMVP.
Как уже упоминалось, это типичный способ портирования языков на новые архитектуры, так что это логично по обычной причине — вы можете переиспользовать практически весь существующий код виртуальной машины, включая реализацию языка и оптимизации. Однако оказывается, что у этого подхода есть несколько особенностей, специфичных для Wasm, которые являются недостатками, и здесь может помочь WasmGC.
Подход портирования на основе WasmGC
Кратко говоря, предложение по сборке мусора для WebAssembly (“WasmGC”) позволяет определять типы структур и массивов и выполнять операции, такие как создание экземпляров, чтение и запись в поля, приведение типов и т.д. (подробнее см. обзор предложения). Эти объекты управляются собственной реализацией GC виртуальной машины Wasm, что и является основным отличием этого подхода от традиционного подхода портирования.
Может помочь так мысленно представить: Если традиционный подход к переносу языка заключается в переносе его на архитектуру, то подход WasmGC очень похож на перенос языка на виртуальную машину (VM). Например, если вы хотите перенести Java на JavaScript, вы можете использовать компилятор, такой как J2CL, который представляет объекты Java как объекты JavaScript, и эти объекты JavaScript управляются виртуальной машиной JavaScript так же, как и все остальные. Перенос языков на существующие виртуальные машины — это полезная техника, примером чего являются все языки, компилируемые в JavaScript, JVM и CLR.
Эта метафора архитектуры/виртуальной машины не является точной, особенно потому, что WasmGC стремится быть более низкоуровневым, чем другие упомянутые нами виртуальные машины. Тем не менее, WasmGC определяет управляемые виртуальной машиной структуры и массивы, а также систему типов для описания их форм и отношений, а перенос на WasmGC составляет процесс представления конструкций вашего языка с помощью этих примитивов; это определённо более высокоуровневый подход, чем традиционный перенос на WasmMVP (где всё сводится к не типизированным байтам в линейной памяти). Таким образом, WasmGC во многом похож на перенос языков на виртуальные машины и обладает преимуществами таких переносов, в частности, хорошей интеграцией с целевой виртуальной машиной и повторным использованием её оптимизаций.
Сравнение двух подходов
Теперь, когда у нас есть представление о двух подходах к переносу языков с GC, давайте посмотрим, как они сравниваются между собой.
Доставка кода управления памятью
На практике большой объём кода Wasm выполняется внутри виртуальной машины, которая уже содержит сборщик мусора, что верно для Web, а также таких сред, как Node.js, workerd, Deno и Bun. В таких случаях реализация сборщика мусора добавляет ненужный размер к бинарному файлу Wasm. Фактически, это не просто проблема для языков с GC в WasmMVP, но также и для языков, использующих линейную память, таких как C, C++ и Rust, поскольку код на этих языках, который делает какие-либо интересные выделения, в конечном итоге включает malloc/free
для управления линейной памятью, что требует нескольких килобайт кода. Например, dlmalloc
требует 6К, а даже malloc, который обменивает скорость на размер, такой как emmalloc
, занимает более 1К. WasmGC, с другой стороны, позволяет виртуальной машине управлять памятью автоматически, так что нам не нужно никакого кода управления памятью — ни GC, ни malloc/free
— в Wasm. В ранее упомянутой статье о WasmGC размер теста fannkuch
оказался гораздо меньшим для WasmGC по сравнению с C или Rust — 2.3 K против 6.1-9.6 K — именно по этой причине.
Сбор циклов
В браузерах Wasm часто взаимодействует с JavaScript (и через JavaScript, с Web API), но в WasmMVP (и даже с предложением reference types) нет возможности создать двунаправленные связи между Wasm и JS, которые позволяли бы тонко собирать циклы. Связи с объектами JavaScript можно размещать только в таблице Wasm, а связи обратно в Wasm могут указывать только на весь Wasm-модуль как на один большой объект, вот так:
Этого недостаточно для эффективного сбора конкретных циклов объектов, некоторые из которых находятся в скомпилированной виртуальной машине, а другие в JavaScript. С WasmGC, с другой стороны, мы можем определить объекты Wasm, о которых виртуальная машина знает, и, таким образом, можем устанавливать правильные связи из Wasm в JavaScript и обратно:
Ссылки GC на стеке
Языки с GC должны учитывать ссылки на стеке, то есть из локальных переменных в области вызова, так как такие ссылки могут быть единственной причиной, по которой объект всё ещё существует. В традиционном переносе языка с GC это является проблемой, потому что изоляция Wasm препятствует программам проверять их собственный стек. Есть решения для традиционных переносов, такие как теневой стек (который может быть реализован автоматически), или сборка мусора только тогда, когда стек пуст (что имеет место между вызовами в цикле событий JavaScript). Возможным будущим улучшением, которое могло бы помочь традиционным переносам, могло бы быть сканирование стека в Wasm. На данный момент только WasmGC может обрабатывать ссылки на стеке без накладных расходов, и делает это полностью автоматически, так как виртуальная машина Wasm управляет GC.
Эффективность GC
Связанной проблемой является эффективность выполнения сборки мусора (GC). Оба подхода портирования имеют потенциальные преимущества в этом отношении. Традиционный порт может повторно использовать оптимизации в существующей виртуальной машине (VM), которые могут быть адаптированы к конкретному языку, например, нацеленными на оптимизацию внутренних указателей или объектов с коротким временем жизни. Порт WasmGC, работающий в интернете, с другой стороны, имеет преимущество в использовании всех наработок, сделанных для ускорения сборки мусора в JavaScript, включая такие техники, как поколенческая сборка мусора, пошаговая сборка и другие. WasmGC также оставляет сборку мусора на усмотрение VM, что упрощает такие вещи, как эффективные барьеры записи.
Еще одно преимущество WasmGC в том, что сборщик мусора может учитывать такие вещи, как давление на память, и соответствующим образом настраивать размер кучи и частоту сборки, как это уже делают виртуальные машины JavaScript в интернете.
Фрагментация памяти
Со временем и особенно в длительно работающих программах операции malloc/free
на линейной памяти WasmMVP могут вызывать фрагментацию. Представьте, что у нас есть 2 МБ памяти, и прямо посередине находится небольшой выделенный блок памяти размером всего в несколько байт. В таких языках, как C, C++ и Rust, невозможно переместить произвольное выделение памяти во время выполнения, и таким образом у нас остается почти 1 МБ слева от этого блока и почти 1 МБ справа. Но это две отдельные области, и поэтому, если мы попробуем выделить 1,5 МБ, мы потерпим неудачу, несмотря на то, что у нас есть такое количество общей невыделенной памяти:
Такая фрагментация может заставить модуль Wasm чаще увеличивать память, что увеличивает задержки и может вызывать ошибки недостатка памяти; улучшения разрабатываются, но это сложная задача. Это проблема всех программ WasmMVP, включая традиционные порты языков с GC (заметим, что сами объекты GC могут быть перемещаемыми, но не части самого времени выполнения). WasmGC, с другой стороны, избегает этой проблемы, поскольку память полностью управляется виртуальной машиной, которая может перемещать их для уплотнения кучи GC и предотвращения фрагментации.
Интеграция инструментов разработчика
В традиционных портах для WasmMVP объекты помещаются в линейную память, предоставляя разработчикам инструментам сложность в представлении полезной информации, так как такие инструменты видят только байты и не имеют сведений о высокоуровневых типах. В WasmGC, с другой стороны, виртуальная машина управляет объектами GC, что позволяет добиться лучшей интеграции. Например, в Chrome можно использовать профилировщик кучи для измерения использования памяти программой WasmGC:
На рисунке выше показана вкладка "Память" в Chrome DevTools, где представлен снимок кучи страницы, на которой запущен код WasmGC, создавший 1,001 небольшой объект в связанном списке. Вы можете видеть имя типа объекта, $Node
, и поле $next
, которое относится к следующему объекту в списке. Представлена вся обычная информация о снимке кучи, такая как количество объектов, поверхностный размер, удерживаемый размер и так далее, что позволяет легко определить, сколько памяти фактически используется объектами WasmGC. Другие функции Chrome DevTools, такие как отладчик, также работают с объектами WasmGC.
Семантика языка
Когда вы перекомпилируете виртуальную машину в традиционном порте, вы получаете язык, который вы ожидали, так как запускаете знакомый код, реализующий этот язык. Это большое преимущество! В сравнении, при порте для WasmGC вы можете рассматривать компромиссы в семантике в обмен на эффективность. Это связано с тем, что в WasmGC мы определяем новые типы GC—структуры и массивы—и компилируем их. В результате мы не можем просто перекомпилировать виртуальную машину, написанную на C, C++, Rust или подобных языках, в эту форму, так как такие языки компилируются только в линейную память, и WasmGC не может помочь с большинством существующих баз кода VМ. Вместо этого, в порте для WasmGC обычно пишется новый код, преобразующий конструкции вашего языка в примитивы WasmGC. И существует множество способов выполнения такого преобразования с различными компромиссами.
Нужны ли компромиссы или нет, зависит от того, как конструкции конкретного языка могут быть реализованы в WasmGC. Например, поля структур WasmGC имеют фиксированные индексы и типы, так что языки, желающие получать доступ к полям более динамичным образом, могут столкнуться с трудностями; существуют разные способы их обхода, и в этом пространстве решений некоторые варианты могут быть проще или быстрее, но не поддерживать полную оригинальную семантику языка. (У WasmGC также есть текущие ограничения, например, отсутствуют внутренние указатели; со временем такие вещи, как ожидается, будут улучшены.)
Как мы уже упоминали, компиляция в WasmGC похожа на компиляцию в существующую виртуальную машину (VM), и существует много примеров компромиссов, которые имеют смысл в таких портах. Например, числа в dart2js (Dart, скомпилированный в JavaScript) ведут себя иначе, чем в виртуальной машине Dart, а строки в IronPython (Python, скомпилированный в .NET) ведут себя как строки C#. В результате не все программы языка могут работать в таких портах, но у этих решений есть веские причины: реализация чисел dart2js как чисел JavaScript позволяет виртуальным машинам эффективно их оптимизировать, а использование строк .NET в IronPython означает, что вы можете передавать эти строки в другой код .NET без дополнительных затрат.
Хотя могут потребоваться компромиссы в портах WasmGC, WasmGC также имеет некоторые преимущества в качестве цели компиляции, особенно по сравнению с JavaScript. Например, хотя dart2js имеет числовые ограничения, о которых мы только что упомянули, dart2wasm (Dart, скомпилированный в WasmGC) ведет себя точно так, как должен, без компромиссов (что возможно, поскольку у Wasm есть эффективные представления для числовых типов, требуемых Dart).
Почему это не является проблемой для традиционных портов? Просто потому, что они перекомпилируют существующую виртуальную машину в линейную память, где объекты хранятся в нетипизированных байтах, что является более низкоуровневым по сравнению с WasmGC. Когда у вас есть только нетипизированные байты, у вас больше гибкости для выполнения разных низкоуровневых (и потенциально небезопасных) трюков, а перекомпилируя существующую виртуальную машину, вы получаете все трюки, которые эта виртуальная машина имеет в своем арсенале.
Усилия по созданию инструментов
Как мы упомянули в предыдущем разделе, порт WasmGC не может просто перекомпилировать существующую виртуальную машину. Вы, возможно, сможете использовать повторно определенный код (например, логику парсера и оптимизации AOT, так как они не взаимодействуют с GC во время выполнения), но в общем случае порты WasmGC требуют значительного объема нового кода.
Для сравнения, традиционные порты на WasmMVP могут быть проще и быстрее: например, вы можете скомпилировать виртуальную машину Lua (написанную на C) в Wasm всего за несколько минут. Однако порт WasmGC для Lua потребовал бы больше усилий, так как вам нужно было бы написать код для преобразования конструкций Lua в структуры и массивы WasmGC, а также решить, как именно делать это в рамках конкретных ограничений системы типов WasmGC.
Таким образом, большие усилия по созданию инструментов являются значительным недостатком портирования на WasmGC. Однако, учитывая все преимущества, о которых мы упоминали ранее, мы считаем, что WasmGC по-прежнему очень привлекателен! Идеальная ситуация заключалась бы в том, чтобы система типов WasmGC могла эффективно поддерживать все языки, и все языки выполняли работу по реализации порта WasmGC. Первая часть этого будет облегчена будущими дополнениями к системе типов WasmGC, а для второй мы можем уменьшить объем работы, связанной с портами WasmGC, максимально делясь усилиями со стороны инструментов. К счастью, оказывается, что WasmGC делает очень практичным дележку работы над инструментами, о чем мы поговорим в следующем разделе.
Оптимизация WasmGC
Мы уже упоминали, что порты WasmGC имеют потенциальные преимущества в скорости, такие как использование меньшего объема памяти и повторное использование оптимизаций в хостовом сборщике мусора. В этом разделе мы покажем другие интересные преимущества оптимизации WasmGC над WasmMVP, которые могут существенно повлиять на то, как проектируются порты WasmGC и насколько быстры итоговые результаты.
Ключевой момент здесь заключается в том, что WasmGC является более высокоуровневым по сравнению с WasmMVP. Чтобы интуитивно понять это, помните, что мы уже говорили, что традиционный порт на WasmMVP похож на портирование на новую архитектуру, а порт WasmGC похож на портирование на новую виртуальную машину, а виртуальные машины, конечно, являются высокоуровневыми абстракциями по сравнению с архитектурами — и высокоуровневые представления обычно более оптимизируемы. Это можно, возможно, увидеть более наглядно с конкретным примером в псевдокоде:
func foo() {
let x = allocate<T>(); // Создание объекта GC.
x.val = 10; // Установить поле в 10.
let y = allocate<T>(); // Создать другой объект.
y.val = x.val; // Это должно быть 10.
return y.val; // Это тоже должно быть 10.
}
Как показано в комментариях, x.val
будет содержать 10
, как и y.val
, так что окончательный возврат будет 10
, а затем оптимизатор даже может удалить выделения, что приведет к следующему:
func foo() {
return 10;
}
Отлично! К сожалению, однако, это невозможно в WasmMVP, потому что каждое выделение превращается в вызов malloc
, большую и сложную функцию в Wasm, которая оказывает побочные эффекты на линейную память. В результате этих побочных эффектов оптимизатор должен предположить, что второе выделение (для y
) может изменить x.val
, который также находится в линейной памяти. Управление памятью сложно, и когда мы реализуем его внутри Wasm на низком уровне, наши возможности для оптимизации ограничены.
В отличие от этого, в WasmGC мы работаем на более высоком уровне: каждое выделение выполняет инструкцию struct.new
, операцию виртуальной машины, которую мы можем фактически анализировать, и оптимизатор может также отслеживать ссылки, чтобы заключить, что x.val
записан ровно один раз со значением 10
. В результате мы можем оптимизировать эту функцию до простого возвращения 10
, как ожидалось!
Помимо выделений, другие элементы, добавляемые WasmGC, включают явные указатели на функции (ref.func
) и вызовы с их использованием (call_ref
), типы в полях структур и массивов (в отличие от нетипизированной линейной памяти) и другие. В результате WasmGC является высокоуровневым промежуточным представлением (IR) по сравнению с WasmMVP и гораздо более оптимизируемым.
Если у WasmMVP ограниченные возможности для оптимизации, почему он работает так быстро? Wasm, в конечном счете, может работать почти на полной скорости нативного кода. Это связано с тем, что WasmMVP, как правило, является результатом работы мощного оптимизирующего компилятора, такого как LLVM. LLVM IR, как и WasmGC, но в отличие от WasmMVP, имеет специальное представление для выделений памяти и т. д., что позволяет LLVM оптимизировать обсуждаемые нами аспекты. Дизайн WasmMVP предполагает, что большинство оптимизаций происходит на уровне инструментария до Wasm, а виртуальные машины Wasm выполняют только "последний этап" оптимизации (такие как распределение регистров).
Может ли WasmGC принять аналогичную модель инструментария, как WasmMVP, и в частности использовать LLVM? К сожалению, нет, поскольку LLVM не поддерживает WasmGC (определённый объем поддержки был изучен, но трудно представить, как могла бы работать полная поддержка). Кроме того, многие языки с поддержкой сборки мусора не используют LLVM — в этой области существует множество различных цепочек компиляторов. Поэтому для WasmGC требуется другой подход.
К счастью, как уже упоминалось, WasmGC легко поддаётся оптимизации, что открывает новые возможности. Вот один из способов взглянуть на это:
Оба рабочих процесса, WasmMVP и WasmGC, начинаются с одних и тех же двух блоков слева: мы начинаем с исходного кода, который обрабатывается и оптимизируется специфическим для языка способом (ведь каждый язык лучше всего знает свои особенности). Затем появляется различие: для WasmMVP сначала нужно выполнить оптимизации общего назначения, после чего преобразовать в Wasm, в то время как для WasmGC есть возможность сначала преобразовать в Wasm, а затем оптимизировать. Это важно, потому что оптимизация после преобразования даёт большое преимущество: тогда мы можем использовать общий код инструментария для оптимизаций общего назначения для всех языков, компилирующихся в WasmGC. Следующий рисунок показывает, как это выглядит:
Поскольку мы можем выполнять общие оптимизации после компиляции в WasmGC, оптимизатор Wasm-to-Wasm может помочь всем инструментам компиляции для WasmGC. По этой причине команда V8 инвестировала в поддержку WasmGC в Binaryen, которым все инструменты могут пользоваться в качестве команды wasm-opt
. Мы сосредоточимся на этом в следующем разделе.
Оптимизации инструментария
Binaryen, проект оптимизации инструментария WebAssembly, уже имел широкий спектр оптимизаций для контента WasmMVP, таких как инлайнинг, распространение констант, устранение мертвого кода и т. д., практически все из которых также применимы к WasmGC. Однако, как уже упоминалось, WasmGC позволяет нам проводить гораздо больше оптимизаций, чем WasmMVP, и мы написали много новых оптимизаций соответственно:
- Анализ утечки для переноса выделений памяти из кучи в локальные переменные.
- Девиртуализация для преобразования косвенных вызовов в прямые (которые затем можно инлайнить, потенциально).
- Более мощное глобальное устранение мертвого кода.
- Анализ потоков содержимого программы с учетом типов (GUFA).
- Оптимизация кастов, например, удаление избыточных кастов и их перемещение ближе к началу.
- Урезание типов.
- Объединение типов.
- Уточнение типов (для локальных переменных, глобальных переменных, полей и сигнатур).
Это лишь краткий перечень некоторых из выполненных нами работ. Для получения дополнительной информации о новых оптимизациях GC в Binaryen и их использовании, см. документацию Binaryen.
Чтобы оценить эффективность всех этих оптимизаций в Binaryen, давайте рассмотрим производительность Java с использованием и без использования wasm-opt
на выходных данных компилятора J2Wasm, который компилирует Java в WasmGC:
Здесь "без wasm-opt" означает, что мы не запускаем оптимизации Binaryen, но всё же выполняем оптимизацию в виртуальной машине и компиляторе J2Wasm. Как показано на рисунке, wasm-opt
обеспечивает значительное ускорение на каждом из этих тестов, в среднем делая их в 1.9× быстрее.
В итоге, wasm-opt
может использоваться любой цепочкой инструментов, которая компилирует в WasmGC, избегая необходимости повторной реализации общих оптимизаций в каждой. И, по мере улучшения оптимизаций Binaryen, выгоды получат все цепочки инструментов, использующие wasm-opt
, так же как улучшения LLVM помогают всем языкам, компилирующимся в WasmMVP с использованием LLVM.
Оптимизации цепочек инструментов — это лишь часть картины. Как мы увидим далее, оптимизации в виртуальных машинах Wasm также крайне важны.
Оптимизации в V8
Как мы уже упоминали, WasmGC более оптимизируем, чем WasmMVP, и это приносит пользу не только цепочкам инструментов, но и виртуальным машинам. Это оказывается важным, поскольку языки с поддержкой сборщика мусора отличаются от языков, компилирующихся в WasmMVP. Рассмотрим, например, инлайнинг, который является одной из самых важных оптимизаций: такие языки, как C, C++ и Rust, выполняют инлайнинг при компиляции, тогда как языки с поддержкой сборщика мусора, такие как Java и Dart, обычно запускаются в виртуальной машине, которая выполняет инлайнинг и оптимизацию во время выполнения. Эта модель производительности влияет как на дизайн языков, так и на то, как люди пишут код на языках с поддержкой сборщика мусора.
Например, в языке, таком как Java, все вызовы начинаются как косвенные (класс-потомок может переопределить функцию родителя, даже если вызов происходит через ссылку на тип родителя). Мы выигрываем всякий раз, когда цепочка инструментов может преобразовать косвенный вызов в прямой, но на практике сценарии реального кода на языке Java часто имеют пути, которые фактически содержат множество косвенных вызовов, или, по крайней мере, те, которые невозможно статически вывести как прямые. Для эффективной обработки таких случаев мы внедрили спекулятивный инлайнинг в V8, то есть косвенные вызовы отмечаются во время выполнения, и если мы замечаем, что вызов имеет довольно простое поведение (мало целевых точек вызова), то выполняем инлайнинг с соответствующими проверками. Это ближе к тому, как Java обычно оптимизируется, чем если бы мы полностью оставили это на усмотрение цепочки инструментов.
Данные реального мира подтверждают этот подход. Мы измерили производительность на движке вычислений Google Sheets Calc Engine, который является кодовой базой Java, используемой для вычисления формул электронных таблиц. До сих пор он компилировался в JavaScript с использованием J2CL. Команда V8 сотрудничает с Sheets и J2CL для портирования этого кода на WasmGC, как из-за ожидаемых преимуществ производительности для Sheets, так и для предоставления полезной обратной связи для процесса спецификации WasmGC. На примере производительности выяснилось, что спекулятивный инлайнинг является самой значительной индивидуальной оптимизацией, которую мы внедрили для WasmGC в V8, как показано на следующей диаграмме:
«Другие оптимизации» здесь означают оптимизации, кроме спекулятивного инлайнинга, которые мы могли отключить для измерения, включая: устранение загрузок, оптимизацию на основе типов, устранение ветвлений, свёртку констант, анализ бегства и устранение общей подвыражения. «Без оптимизаций» означает, что мы отключили все это, а также спекулятивный инлайнинг (но другие оптимизации в V8 остаются, которые мы не можем легко отключить; поэтому числа здесь являются лишь приблизительными). Очень большое улучшение благодаря спекулятивному инлайнингу — примерно 30% повышение скорости (!) — по сравнению со всеми другими оптимизациями вместе взятыми показывает, насколько важен инлайнинг, по крайней мере, для компилированной Java.
Кроме спекулятивного инлайнинга, WasmGC опирается на существующую поддержку Wasm в V8, что означает, что он выигрывает от существующего конвейера оптимизатора, распределения регистров, тирации и так далее. В дополнение ко всему этому, отдельные аспекты WasmGC могут выиграть от дополнительных оптимизаций, наиболее очевидные из которых — это оптимизация новых инструкций, которые обеспечивает WasmGC, например, эффективная реализация приведения типов. Еще одной важной задачей, которую мы выполнили, является использование информации о типах WasmGC в оптимизаторе. Например, ref.test
проверяет, является ли ссылка определенного типа во время выполнения, и после успешного выполнения такой проверки мы знаем, что ref.cast
, приведение к тому же типу, также должно быть успешным. Это помогает оптимизировать такие шаблоны, как этот в Java:
if (ref instanceof Type) {
foo((Type) ref); // Этот нисходящий каст может быть устранен.
}
Эти оптимизации особенно полезны после спекулятивного инлайнинга, потому что тогда мы видим больше, чем цепочка инструментов видела, когда производила Wasm.
В целом, в WasmMVP была довольно четкая граница между оптимизациями цепочки инструментов и виртуальной машины: мы делали как можно больше в цепочке инструментов и оставляли только необходимые вещи для виртуальной машины, что имело смысл, так как это упрощало виртуальные машины. С WasmGC этот баланс может несколько измениться, потому что, как мы видим, необходимо выполнять больше оптимизаций во время выполнения для языков с поддержкой сборщика мусора, а также WasmGC сам по себе более оптимизируем, что позволяет иметь больше пересечений между оптимизациями цепочки инструментов и виртуальной машины. Будет интересно увидеть, как эта экосистема будет развиваться.
Демонстрация и статус
Вы можете начать пользоваться WasmGC уже сегодня! После достижения фазы 4 в W3C, WasmGC теперь является полноценным и окончательно утвержденным стандартом, и Chrome 119 выпущен с поддержкой данного стандарта. С этим браузером (или любым другим браузером, поддерживающим WasmGC, например, Firefox 120, который ожидается к запуску с поддержкой WasmGC позднее в этом месяце) вы можете запустить этот демо-пример Flutter, где Dart, скомпилированный в WasmGC, отвечает за логику приложения, включая виджеты, макет и анимацию.
Начало работы
Если вас заинтересовал WasmGC, следующие ссылки могут быть полезны:
- На сегодняшний день различные инструментарии поддерживают WasmGC, включая Dart, Java (J2Wasm), Kotlin, OCaml (wasm_of_ocaml), и Scheme (Hoot).
- Исходный код небольшой программы, результат которой мы показали в разделе инструментов для разработчиков, является примером ручного написания программы "hello world" на WasmGC. (В частности, можно увидеть определение типа
$Node
, а затем его создание с использованиемstruct.new
.) - Вики Binaryen предоставляет документацию о том, как компиляторы могут генерировать код WasmGC с хорошей оптимизацией. Более ранние ссылки на различные инструментарии, ориентированные на WasmGC, также могут быть полезны для изучения. Например, вы можете ознакомиться с проходами и параметрами Binaryen, которые используют Java, Dart и Kotlin.
Резюме
WasmGC — это новый и перспективный способ реализации языков с автоматическим управлением памятью в WebAssembly. Традиционные методы портирования, в которых виртуальная машина перекомпилируется в Wasm, все еще будут полезны в некоторых случаях, но мы надеемся, что портирование через WasmGC станет популярной техникой благодаря своим преимуществам: порты через WasmGC имеют возможность быть меньшего размера, чем традиционные порты — и даже меньше, чем программы, написанные на WasmMVP с использованием C, C++ или Rust, а также они лучше интегрируются с веб-средой в таких вопросах, как сборка циклов, использование памяти, инструменты разработчика и многое другое. WasmGC также представляет собой более оптимизируемое представление, что может обеспечить значительное увеличение скорости и дополнительные возможности для совместного использования усилий между языками в рамках инструментальной цепочки.