Перейти к основному содержимому

Emscripten и LLVM WebAssembly backend

· 12 мин. чтения
Алон Закай

WebAssembly обычно компилируется из исходного языка, что означает, что разработчикам нужны инструменты для его использования. По этой причине команда V8 работает над соответствующими open-source проектами, такими как LLVM, Emscripten, Binaryen и WABT. В этом посте описана часть работы, которая выполнена над Emscripten и LLVM, и которая скоро позволит Emscripten по умолчанию перейти на LLVM WebAssembly backend — пожалуйста, протестируйте его и сообщите о любых проблемах!

LLVM WebAssembly backend уже некоторое время доступен в Emscripten, поскольку мы работали над backend параллельно с его интеграцией в Emscripten, в сотрудничестве с другими участниками сообщества инструментов WebAssembly с открытым исходным кодом. Теперь он достиг стадии, где WebAssembly backend превосходит старый backend “fastcomp” по большинству метрик, и поэтому мы хотели бы сделать его стандартным. Это объявление делается заранее, чтобы как можно больше протестировать его.

Это важное обновление по нескольким волнующим причинам:

  • Гораздо более быстрая сборка: LLVM WebAssembly backend в сочетании с wasm-ld полностью поддерживает инкрементную компиляцию с использованием объектных файлов WebAssembly. Fastcomp использовал LLVM IR в биткод-файлах, что означало, что во время сборки весь IR должен быть скомпилирован LLVM. Это было основной причиной медленной сборки. С объектными файлами WebAssembly .o файлы уже содержат скомпилированный WebAssembly (в перемещаемой форме, которая может быть связана, практически как при работе с нативной сборкой). В результате этап сборки может быть намного быстрее, чем с fastcomp — ниже мы покажем измерение реального мира с увеличением скорости в 7 раз!
  • Более быстрый и компактный код: Мы усердно работали над LLVM WebAssembly backend, а также над оптимизатором Binaryen, который запускается в Emscripten. Результатом является то, что путь через LLVM WebAssembly backend теперь превосходит fastcomp как по скорости, так и по размеру в большинстве отслеживаемых нами тестов.
  • Поддержка всего LLVM IR: Fastcomp мог обработать LLVM IR, сгенерированный clang, но из-за своей архитектуры он часто терпел неудачу на других источниках, особенно при “легализации” IR в типы, которые мог обработать fastcomp. С другой стороны, LLVM WebAssembly backend использует общую инфраструктуру backend LLVM, поэтому он может обрабатывать все.
  • Поддержка новых функций WebAssembly: Fastcomp компилирует в asm.js перед запуском asm2wasm, что затрудняет поддержку новых функций WebAssembly, таких как хвостовые вызовы, исключения, SIMD и других. WebAssembly backend является естественным местом для работы над этими функциями, и мы фактически уже работаем над всем вышеупомянутым!
  • Более быстрые обновления из ветки upstream: В связи с предыдущим пунктом, использование upstream WebAssembly backend означает, что мы можем использовать самую последнюю ветку LLVM upstream в любое время, что дает возможность получить новые функции языка C++ в clang, новые оптимизации LLVM IR и т.д. как только они появляются.

Тестирование

Чтобы протестировать WebAssembly backend, просто используйте последнюю версию emsdk и выполните

emsdk install latest-upstream
emsdk activate latest-upstream

“Upstream” здесь означает, что LLVM WebAssembly backend находится в upstream LLVM, в отличие от fastcomp. Фактически, поскольку он находится в upstream, вам не нужно использовать emsdk, если вы сами собираете чистый LLVM+clang! (Чтобы использовать такую сборку с Emscripten, просто добавьте путь к ней в ваш файл .emscripten.)

В настоящее время использование emsdk [install|activate] latest все еще использует fastcomp. Также есть “latest-fastcomp”, который делает то же самое. Когда мы переключим стандартный backend, мы сделаем “latest” таким же, как “latest-upstream”, и тогда “latest-fastcomp” будет единственным способом получить fastcomp. Fastcomp остается вариантом, пока он по-прежнему полезен; дополнительные заметки об этом приведены в конце.

История

Это будет третий бэкенд в Emscripten и второй переход. Первый бэкенд был написан на JavaScript и парсил LLVM IR в текстовом формате. Это было полезно для экспериментов в 2010 году, но имело очевидные недостатки, включая то, что текстовый формат LLVM мог изменяться, а скорость компиляции была недостаточно высокой. В 2013 году был написан новый бэкенд на основе форка LLVM, получивший прозвище "fastcomp". Он был разработан для генерации asm.js, к чему был адаптирован предыдущий JS-бэкенд (хотя делал это не очень хорошо). В результате это стало большим улучшением качества кода и времени компиляции.

Это было также относительно небольшим изменением в Emscripten. Несмотря на то, что Emscripten — это компилятор, оригинальный бэкенд и fastcomp всегда были достаточно небольшой частью проекта — гораздо больше кода включают системные библиотеки, интеграция с инструментарием, привязки языка и так далее. Так что, хотя переход на новый бэкенд компилятора — это драматическое изменение, оно затрагивает только одну часть всего проекта.

Бенчмарки

Размер кода

Измерения размера кода (меньше — лучше)

(Все размеры здесь нормализованы для fastcomp.) Как видно, размеры с бэкендом WebAssembly почти всегда меньше! Разница более заметна на меньших микротестах слева (имена написаны строчными буквами), где новые улучшения системных библиотек имеют большее значение. Однако уменьшение размера кода наблюдается даже у большинства макротестов справа (имена в ЗАГЛАВНЫХ БУКВАХ), которые представляют собой реальные кодовые базы. Единственный регресс у макротестов — LZMA, где в более новой версии LLVM принимается другое решение по инлайнингу, которое оказывается неудачным.

В общем, макротесты уменьшаются в среднем на 3,7%. Неплохо для обновления компилятора! Мы видим аналогичное на реальных кодовых базах, которые не входят в тестовый набор, например, BananaBread, порт игрового движка Cube 2 на веб, уменьшается более чем на 6%, а Doom 3 уменьшается на 15%!

Эти улучшения размера (и улучшения скорости, которые мы обсудим далее) обусловлены несколькими факторами:

  • Генерация кода в бэкенде LLVM умна и может делать то, что простые бэкенды, такие как fastcomp, не могут, например, GVN.
  • Новейший LLVM имеет лучшие оптимизации IR.
  • Мы много работали над настройкой оптимизатора Binaryen для вывода бэкенда WebAssembly, как упоминалось ранее.

Скорость

Измерения скорости (меньше — лучше)

(Измерения проводились на V8.) Среди микротестов скорость — это смешанная картина — что не удивительно, так как большинство из них доминируются одной функцией или даже циклом, поэтому любое изменение кода, генерируемого Emscripten, может привести к удачному или неудачному выбору оптимизации виртуальной машины. В целом наблюдается примерно одинаковое количество микротестов, которые остаются неизменными, улучшаются или регрессируют. Если взглянуть на более реалистичные макротесты, снова LZMA — это аномалия, опять же из-за неудачного решения по инлайнингу, как упоминалось ранее, но в остальном каждая макротестация улучшена!

Среднее изменение у макротестов — ускорение на 3,2%.

Время сборки

Измерения времени компиляции и линковки на BananaBread (меньше — лучше)

Изменения времени сборки будут различаться от проекта к проекту, но вот некоторые примеры данных из BananaBread, который является полным, но компактным игровым движком, состоящим из 112 файлов и 95,287 строк кода. Слева показано время сборки для шага компиляции, то есть компиляции исходных файлов в объектные файлы, с использованием стандартного параметра проекта -O3 (все времена нормализованы для fastcomp). Как видно, шаг компиляции занимает немного больше времени с бэкендом WebAssembly, что логично, потому что мы выполняем больше работы на этом этапе — вместо того, чтобы просто быстро компилировать исходники в биткод, как делает fastcomp, мы также компилируем биткод в WebAssembly.

Справа показаны данные для шага линковки (тоже нормализованы для fastcomp), то есть создания окончательного исполняемого файла, здесь с -O0, что подходит для инкрементальной сборки (для полностью оптимизированной сборки, вероятно, будет использоваться и -O3, как упомянуто ниже). Оказывается, что небольшое увеличение во время шага компиляции того стоит, потому что линковка более чем в 7 раз быстрее! Это настоящее преимущество инкрементальной компиляции: большая часть шага линковки — это просто быстрое объединение объектных файлов. А если вы меняете только один исходный файл и пересобираете, то почти всё, что вам нужно — это этот быстрый шаг линковки, так что вы можете видеть это ускорение всё время в реальной разработке.

Как упоминалось выше, изменения времени сборки будут варьироваться в зависимости от проекта. В меньшем проекте, чем BananaBread, ускорение времени связки может быть меньше, тогда как в большем проекте оно может быть больше. Другим фактором являются оптимизации: как упоминалось выше, тест был связан с флагом -O0, но для сборки релиза вы, вероятно, захотите использовать флаг -O3, и в этом случае Emscripten вызовет оптимизатор Binaryen для конечного WebAssembly, выполнит meta-dce, а также другие полезные вещи для размера и скорости кода. Конечно, это займет дополнительное время, но для сборки релиза это того стоит — в BananaBread размер WebAssembly сокращается с 2.65 до 1.84 MB, улучшение более чем на 30% — однако для быстрой инкрементальной сборки это можно пропустить, используя -O0.

Известные проблемы

Хотя бекенд LLVM для WebAssembly обычно выигрывает как в размере кода, так и в скорости, мы видели некоторые исключения:

  • Fasta регрессирует без нетраппинг-преобразований float в int, новой функции WebAssembly, которая не была включена в МВП WebAssembly. Основная проблема заключается в том, что в МВП преобразование float в int вызывает исключение, если значение выходит за пределы диапазона допустимых целых чисел. Предположение было, что это поведение в любом случае не определено в C и его легко реализовать в виртуальных машинах. Однако оказалось, что это плохо сочетается с тем, как LLVM компилирует преобразования float в int, что приводит к необходимости использования дополнительных средств защиты, увеличивая размер и накладные расходы кода. Новые нетраппинг-операции избегают этого, но могут быть еще не доступны во всех браузерах. Вы можете использовать их, компилируя исходные файлы с флагом -mnontrapping-fptoint.
  • Бекенд LLVM для WebAssembly не только является другим бекендом, чем fastcomp, но также использует гораздо более новую версию LLVM. Более новая версия LLVM может принимать другие решения о встроении, которые (как и все решения о встроении в отсутствие оптимизации с учетом профилирования) основаны на эвристике и могут оказаться как полезными, так и вредными. Конкретный пример, который мы упоминали ранее, — тест LZMA, где более новая версия LLVM встраивает функцию 5 раз, что в конечном итоге только наносит вред. Если вы столкнулись с подобным в своих проектах, вы можете избирательно компилировать определенные исходные файлы с флагом -Os, чтобы сосредоточиться на размере кода, использовать __attribute__((noinline)) и т.д.

Может быть больше проблем, о которых мы не знаем и которые нужно оптимизировать — пожалуйста, сообщите нам, если вы что-нибудь найдете!

Прочие изменения

Существует небольшое количество функций Emscripten, которые связаны с fastcomp и/или asm.js, что означает, что они не могут работать из коробки с бекендом WebAssembly, и поэтому мы работаем над их альтернативами.

Вывод в JavaScript

Выбор вывода в форме, отличной от WebAssembly, все еще важен в некоторых случаях — хотя все основные браузеры уже давно поддерживают WebAssembly, все еще существует длинный хвост старых компьютеров, старых телефонов и т.д., которые не имеют поддержки WebAssembly. Кроме того, по мере добавления новых функций в WebAssembly эта проблема все равно останется актуальной. Компиляция в JavaScript — это способ гарантировать возможность достичь всех пользователей, даже если сборка не будет такой маленькой или быстрой, как WebAssembly. В fastcomp мы просто использовали вывод asm.js непосредственно для этой цели, но с бекендом WebAssembly очевидно требуется что-то другое. Для этой цели мы используем wasm2js из Binaryen, который, как следует из названия, компилирует WebAssembly в JavaScript.

Вероятно, это заслуживает отдельного блога, но вкратце здесь ключевое решение заключается в том, что нет смысла поддерживать asm.js больше. asm.js может работать гораздо быстрее общего JavaScript, но оказывается, что практически все браузеры, которые поддерживают AOT-оптимизации для asm.js, также поддерживают WebAssembly (фактически, Chrome оптимизирует asm.js, преобразуя его в WebAssembly внутренне!). Таким образом, когда мы говорим о fallback-опции для JavaScript, она может не использовать asm.js; на самом деле это проще, позволяет нам поддерживать больше функций в WebAssembly и также приводит к значительно меньшему JS! Поэтому wasm2js не ориентирован на asm.js.

Однако побочный эффект этого дизайна заключается в том, что если вы тестируете сборку asm.js из fastcomp в сравнении с JS-сборкой через бекенд WebAssembly, то asm.js может быть гораздо быстрее — если вы тестируете в современном браузере с AOT-оптимизацией для asm.js. Это, вероятно, актуально для вашего собственного браузера, но не для браузеров, которым действительно нужен вариант без WebAssembly! Для правильного сравнения следует использовать браузер без оптимизаций для asm.js или с отключенными оптимизациями. Если вывод wasm2js все еще медленнее, пожалуйста, сообщите нам!

В wasm2js отсутствуют некоторые менее используемые функции, такие как динамическое связывание и потоки (pthreads), но большинство кода уже должно работать, и оно было тщательно проверено через fuzz-тестирование. Чтобы протестировать вывод в JS, просто соберите с флагом -s WASM=0, чтобы отключить WebAssembly. emcc затем выполнит wasm2js за вас, и если это оптимизированная сборка, он также выполнит различные полезные оптимизации.

Другие вещи, которые вы можете заметить

  • Опции Asyncify и Emterpreter работают только в fastcomp. Замена разрабатывается на основе входных данных. Мы ожидаем, что это со временем станет улучшением по сравнению с предыдущими опциями.
  • Предварительно построенные библиотеки должны быть перестроены: если у вас есть какая-то library.bc, которая была создана с помощью fastcomp, вам потребуется перестроить её из исходных данных, используя более новую версию Emscripten. Это всегда было необходимо, когда fastcomp обновлял LLVM до новой версии, которая изменяла формат биткода, и текущее изменение (к объектным файлам WebAssembly вместо биткода) оказывает аналогичное влияние.

Заключение

Наша основная цель сейчас — устранить любые ошибки, связанные с этим изменением. Пожалуйста, протестируйте и отправляйте отчеты о проблемах!

После того как всё стабилизируется, мы переключим компилятор по умолчанию на основной бэкенд WebAssembly. Fastcomp останется доступным вариантом, как упоминалось ранее.

Мы хотели бы в конечном итоге полностью удалить fastcomp. Это позволит значительно снизить нагрузку на сопровождение, сосредоточиться на разработке новых функций в бэкенде WebAssembly, ускорить общие улучшения в Emscripten и принести другие преимущества. Пожалуйста, сообщите нам о результатах тестирования на ваших кодовых базах, чтобы мы могли начать планировать сроки удаления fastcomp.

Спасибо

Спасибо всем, кто участвовал в разработке бэкенда LLVM WebAssembly, wasm-ld, Binaryen, Emscripten и других вещей, упомянутых в этом посте! Частичный список этих замечательных людей: aardappel, aheejin, alexcrichton, dschuff, jfbastien, jgravelle, nwilson, sbc100, sunfish, tlively, yurydelendik.