Всем привет! Сейчас за окном осенние деньки 2024 года. Вещает Пройдаков Евгений. Сейчас я руковожу группой разработки среды исполнения языка eXtraction and Processing в R&D департаменте Positive Technologies.
Доменно специфичный язык eXtraction and Processing является важной частью движка поведенческого анализа, используемого в таких продуктах Positive Technologies, как MaxPatrol SIEM и PT ISIM. Сегодня хотелось представить вашему вниманию выжимку нашего R&D процесса в экспериментах с WebAssembly. Узнаем, что такое WebAssembly. Поймём, как его можно встроить в программный продукт. Коснёмся инструментов разработки и сред исполнения WebAssembly. А также в рамках одной статьи пройдём путь от постановки задачи до результатов по разработке среды исполнения для доменно специфичного языка программирования. Кроме того, мы разберем некоторые проблемы, которые могут появиться у вас при попытке собрать и отладить большой С++ проект под WebAssembly. Материал может быть особенно полезен тем, кто хочет использовать WebAssembly за пределами веб‑браузера.
Более подробно про сам язык eXtraction and Processing можно почитать в прошлой статье цикла от моего коллеги Михаила Максимова.
Будем рады всем неравнодушным к теме разработки доменно специфичных языков, компиляторов, сред исполнения. Не важно, опытный вы разработчик, начинающий или только интересуетесь этой темой.
Глоссарий
Определимся с кратким списком терминов и сокращений, чтобы говорить на одном языке:
Определимся с кратким списком терминов и сокращений, чтобы говорить на одном языке:
XP — язык eXtraction and Processing
AST — abstract syntax tree
LLVM — (ранее Low Level Virtual Machine) — проект программной инфраструктуры для создания компиляторов и сопутствующих им утилит
IR — intermediate representation
Wasm — WebAssembly
WAMR — WebAssembly Micro Runtime
WASI — WebAssembly System Interface
WABT — The WebAssembly Binary Toolkit
JIT — just‑in‑time компиляция
IOT — Internet of Things
AOT — ahead‑of‑time компиляция
ABI — application binary interface
EPS — events per second (количество событий в секунду)
BUILTIN — функция стандартной библиотеки
DSL — domain specific language
DOM — document object model
ELF — executable and linkable format
SDK — software development kit
SIMD — single instruction, multiple data
Sanitizer (санитайзер) — средство для выявления ошибок в среде выполнения
Пятиминутка Wasm
WebAssembly — это новый открытый формат байт‑кода, исполняемого современными браузерами. Он позволяет переносить код, написанный на таких языках как C, C++, Rust, в низкоуровневые ассемблерные инструкции и использовать его в сети. Формат имеет компактные размеры, высокую производительность, близкую к нативной, и в браузере может одновременно работать с JavaScript.
По своей сути WebAssembly представляет стековую машину, все указатели внутри которой не способны выйти за границы песочницы — выделенного региона памяти. Данный способ работы с памятью не позволяет испортить код или данные вне региона песочницы. В случае пробоя памяти будут испорчены только данные стека, кучи или таблицы косвенных вызовов, используемых экземпляром Wasm.
Также WebAssembly предоставляет механизм импорта нативных функций. В качестве параметров таких функций могут фигурировать тривиальные типы или последовательные блоки памяти. Внутри нативной функции в момент вызова адреса Wasm будут автоматически пересчитаны в виртуальные адреса хост машины. С помощью этого механизма обеспечивается гибкое взаимодействие с операционной системой: например, работа с файлами, сетевыми дескрипторами или доступ к специфической аппаратуре.
Почитать подробнее про WebAssembly можно здесь. Про механизм нативных функций здесь.
Пример простого модуля с одной функцией на WebAssembly:
(module
(func (export "add") (param $n1 i32) (param $n2 i32) (result i32)
get_local $n1
get_local $n2
i32.add
)
Постановка задачи
Результатом работы нашей команды является пакет инструментов для разработки, который имеет два основных способа использования:
на машинах коллег экспертов во время разработки новой экспертизы в виде консольных исполняемых файлов, с помощью которых можно выполнить разрабатываемые правила и проверить их корректность;
как библиотека внутри больших серверных продуктов с множеством потоков и большим количеством событий в секунду на процессорное ядро.
Я подключился к работе команды в тот момент, когда на руках уже были:
новый компилятор из языка XP в представление LLVM IR;
стандартная библиотека для языка XP написанная на С++ с хорошим покрытием тестами;
среда исполнения на базе LLVM_JIT, способная загружать LLVM IR с динамическим связыванием стандартной библиотеки;
комплект экспертизы в виде, а это примерно 10 тысяч правил для функционального тестирования и проверки производительности;
достаточное количество входных данных с эталонами результатов, чтобы это всё отладить.
В целом решение работало, производительность была близка к исполнителю XP, что уже работает в production. В наличии был набор «детских болячек», ряд неработающих тестов и большой потенциал для ускорения.
Также здесь хочется сжато описать, что делает наш код и какие данные поступают на обработку. На вход XP код получает событие. Его можно представить как DOM объект. Для простоты можно считать его JSON объектом с набором строковых ключей, а также тривиальными и комплексными значениями. Код XP читает поля из входящего события по ключам и выполняет его категоризацию. Далее вызывается конкретный обработчик, который может сгенерировать результирующее событие или вернуть ошибку. Далее для краткости код, генерируемый компилятором из XP, будем называть формулой.
Для группы разработки среды исполнения задача разбивалась на части:
выбрать библиотеку по загрузке и исполнению Wasm кода, реализовать обвязку;
подружить стандартную библиотеку XP, написанную на С++, с Wasm байт кодом формулы;
научиться транслировать XP в Wasm так, чтобы полученный код мог использовать стандартную библиотеку;
отладить это всё и получить итоговые цифры по быстродействию и потребляемым ресурсам;
научиться профилировать полученный код и уметь улучшать его производительность.
Далее по тексту мы не раз будем сравнивать новый исполнитель XP на базе WebAssembly с текущим решением. Давайте для простоты обсуждения зафиксируем, что сейчас XP исполняет проприетарная стековая машина с общей с хостом памятью.
Процесс разработки
Выбор Wasm машины
Для решения первой задачи нужно выбрать одну из сред исполнения Wasm и написать вокруг этого определённый объём обвязки по передаче входных и получения выходных данных. В качестве библиотеки исполнения Wasm мы выбрали WebAssembly Micro Runtime. Библиотека написана на С. Проект активно развивается при поддержке компании Intel и других крупных компаний с экспертизой в разработке низкоуровневых SDK. Поддерживает несколько операционных систем и процессорных архитектур, умеет работать в режимах:
интерпретации;
JIT компиляции;
AOT компиляции.
Также эта среда исполнения в режимах интерпретации и AOT показывает очень компактное потребление памяти в работающем процессе. Из коробки поддерживается многопоточное исполнение в двух режимах:
один экземпляр исполнителя на поток, каждый из которых имеет своё адресное пространство;
кооперативный вариант, когда несколько потоков работают одновременно с одним разделяемым адресным пространством.
Мы сравнивали на небольших тестовых сценариях и другие среды исполнения, но по различным параметрам, а именно: потребление оперативной памяти, простота встраивания в С++ проект, язык, на котором написан исполнитель, количество поддерживаемых платформ, производительность итогового кода. На первом этапе мы не увидели какой‑то серьёзной разницы и остановились на варианте Wasm Micro Runtime. Вернувшись сейчас к этому выбору, рекомендовал бы ещё присмотреться к потребляемым ресурсам на запуске кода проекта, по размерам аналогичного вашему.
Для полноты картины упомяну другие среды исполнения, которые были опробованы нами на начальном этапе выбора: WasmEdge, Wasmtime, V8.
Wasm + стандартная библиотека XP
Для решения второй проблемы нужно было определиться со способом связывания кода, генерируемого компилятором и стандартной библиотекой.
В вопросе связывания стандартной библиотеки и формулы есть два подхода:
код стандартной библиотеки должен быть скомпилирован в Wasm и слинкован с формулой в рамках одной Wasm программы;
выполняем отображение всех используемых формулой функций и оставим реализацию библиотеки в виде нативного кода скомпилированного обычным компилятором С++.
Был проведён детальный анализ реализации методов стандартной библиотеки. Она представляла из себя около сотни C‑ABI функций с передачей простых типов и указателей на структуры с указателями внутри. Нам стало ясно, что отразить все функции в нативный код без существенной модификации не выйдет. Основной причиной здесь является отдельное адресное пространство Wasm. Внутри Wasm32 указатель это 32-битное число, представляющее собой отступ относительно начала адресного пространства, а для хоста это будет 64-битный адрес в виртуальной памяти.
Можно было бы попробовать передавать указатели как числа из нативного мира в Wasm и обратно. Но кто, собственно, гарантирует, что после возвращения в нативный мир указатель всё ещё будет валидным указателем (привет защищённый режим в Эльбрус). Также WAMR в документации категорически запрещает передачу указателей на структуры из Wasm в нативные вызовы по причинам нарушения системы безопасности и несовместимости ABI типов данных в песочнице c типами живущими снаружи.
Необходимость оставить код и данные в одном адресном пространстве подтолкнуло нас к решению компилировать стандартную библиотеку XP в Wasm целиком. Исключение составили несколько методов сохранения результата, которые передавали плоский массив данных на вход и на выход, реализованных через импортирование. Дальше мы увидим ряд проблем со сборкой всего проекта под Wasm, но тогда мы о них не знали. В случае если вы захотите оставить большую часть реализации вне Wasm песочницы, вам придётся переделать всё API на дескрипторах и плоских массивах данных как в интерфейсах операционных систем.
Это задача решаемая не для любого типа продукта и подобная переделка с проверками будет стоить достаточно процессорного времени. Если пойти по пути отображения большого числа функций из нативного мира, то такой архитектурный подход ставит крест на работе в браузерах. Возможность зайти на веб‑страничку, отредактировать код онлайн и после небольшой компиляции на удалённом сервере проводить отладку и тестирование вызывала восторг у нашей команды разработки экспертизы. Мы не хотели хоронить такую возможность на старте проекта.
Подобным образом поступают разработчики UnrealEngine.
Наличие большого количества нативных функций, импортируемых в Wasm в виде отдельных модулей, является, скорее, прерогативой IOT устройств. В таких проектах может требоваться обеспечить доступ к самой разнообразной аппаратуре, и компания разработчик сможет сделать своё индивидуальное решение.
Для сборки С++ в WebAssembly мы использовали wasi‑sdk.
Чтобы работать с объектными файлами Wasm, отлично подойдёт wabt.
Собственно, выполнив первые два пункта мы остановились на следующей архитектуре:
В ходе сборки стандартной библиотеки XP под Wasm у нас возник ряд проблем, а именно:
стандартная библиотека языка C wasi‑libc, от которой зависела наша стандартная библиотека XP, собиралась только под 32 бита. Проблемное место живёт тут. Нам сперва пришлось научиться собирать наш код с флагом
-m32
под хост машину и выправить проблемы с размером и выравниванием структур, а также конвертацией типов;поддержка исключений находится в стадии разработки, код можно собирать только с
-fno-exceptions
, благо мы их особенно не использовали — пришлось поправить считанное число мест в библиотеке, также пришлось потрогать зависимости, чтобы они собирались и использовались без исключений. Proposal по исключениям.
В какой‑то момент работы над кодом нам потребовалось прогнать пакет тестов, чтобы убедиться, что все функции среды исполнения работают как задумано. Каково было моё удивление обнаружить не работающие статические переменные. Решением является опция компилятора, изменяющая тип модуля. Её удалось найти достаточно оперативно, надеюсь, вам это также поможет:
-mexec-model=reactor \
Транслируем XP в Wasm
Третья проблема лежала целиком и полностью на стороне команды компилятора. По этой теме можно было легко написать ещё одну полноценную статью. Лишь вскользь затрону несколько возникших бед:
Используемый нами бекенд для WebAssembly не поддерживает computed gotos, которые в ряде случаев генерировал наш компилятор;
Наш исполнитель WAMR ограничивал количество переменных внутри одной Wasm функции, это привело к необходимости добавить проход, разделяющий большие функции на несколько меньшего размера;
Потребление памяти Wasm backend внутри LLVM компилятора. На нашей кодовой базе мы получали более 10 гигабайт на сборке всех правил в один объектный файл. Мы пытались разделить код правил на разные единицы трансляции — это улучшало ситуацию с максимальным потреблением памяти, но увеличивало время компиляции.
Коллеги из группы компилятора подсказывают, что в случае генерации Wasm напрямую без LLVM backend часть проблем можно было избежать. Но подобной детальной работы мы не проводили. Чтобы уйти с LLVM требовалось достаточно много времени. Также мешала необходимость линковки со средой исполнения — была вероятность, что пришлось бы вручную реализовывать генерацию объектных файлов Wasm, потому что формат хоть и условно стандартизован, используется в основном в LLVM.
Также отмечу отдельно, что в случае, если вы собираетесь генерировать Wasm программы руками, без посредника в виде LLVM IR, вам не будет доступна возможность собрать нативный исполняемый файл и огромная экосистема поиска проблем в сгенерированном коде. Системы загрузки и исполнения Wasm кода опираются на некоторые обязательные символы такие как: __wasm_call_ctors
, __wasm_call_dtors
и __stack_pointer
. В случае ручной генерации вам придётся детально изучить спецификацию на среду и самим корректно реализовать поддержку этого функционала. Иначе модуль не сможет быть загружен или будет работать некорректно, как это было с неверным типом модуля у нас из‑за чего «отвалились» все статические переменные и программа аварийно завершалась.
Отладка работы решения
В этой точке на руках у нас была: стандартная библиотека libxp_runtime_wasm.a
, генерируемый из XP объектный файл с формулой решателем formula.o
, а также модуль линкера на базе wasm‑ld. Все подготовительные шаги были сделаны. Теперь можно собрать формулы в единый wasm32 файл и заставить его обрабатывать входящий трафик.
Отдельно отмечу, что теоретически возможность отлаживать код скомпилированный в Wasm существует. На практике нам не удалось это сделать для реального приложения. Детальное описание списка проблем с отладчиком выходят за рамки этой статьи. Для решения проблем отладки отдельных функций стандартной библиотеки и фрагментов генератора мы использовали различные виды тестов, а также отдельную сборку по компиляции нашего приложения в режиме нативного x86 и x64 ассемблера. Нас очень спасала возможность компилятора выдавать LLVM IR. Это позволяло транслировать формулу в объектный файл и получать нативные бинарные файлы со всем набором санитайзеров или других средств по динамическому анализу приложения. Даже не представляю, сколько времени бы ушло на отладку без этого инструмента.
Внезапно возникшей проблемой стал запуск полного набора формул XP. Маленькие файлы с несколькими правилами и частью стандартной библиотеки занимали 4 мегабайта. Файл с проблемой весил около 32 мегабайт. При верификации на тестовых данных мы столкнулись со странным пробоем в куче. Санитайзеры внутрь Wasm затащить нельзя и стало понятно, что придётся отлаживать по‑другому. Мы решили хорошо, а давайте соберём полный граф под нативный x86 процессор и найдём пробой в памяти с помощью санитайзеров. К сожалению в таком варианте санитайзеры не находили проблем с памятью.
Далее мы увеличивали число правил, попадающих в итоговый исполняемый файл. Проблема возникала при масштабировании и воспроизводилась только внутри Wasm кода. Причём характер этого падения был странным, а именно в какой‑то момент ломались виртуальные функции. Вытащив промежуточный исходник из компилятора в виде текстового LLVM IR кода, мы выполнили diff двух файлов. Разница составила несколько повторяющихся операций со стеком. Здесь уже стали закрадываться сомнения, а не происходит ли при росте стека перезапись блока с данными относящимися к работе виртуальных функций.
Внимательное изучение документации на линкер мы нашли несколько занятных опций для манипуляции со стеком:
Подробно об этом можно почитать в документе.
Подобрав достаточный размер стека проблема была пройдена. Команда почувствовала, что мы уже на финишной прямой.
Борьба за производительность
Дальше мы получили первые цифры производительности, они оказались скромнее наших ожиданий. Нас ждали несколько месяцев оптимизаций нашего решения.
В качестве замеров производительности мы использовали два сценария:
консольное однопоточное приложение, полный комплект правил с набором эталонных входных и выходных данных;
серверное многопоточное приложение с теми же тестовыми данными и комплектом правил.
Обе версии утилит позволяли получить некоторое число событий в секунду, обрабатываемых исполнителем. Для максимальной эффективности и сходимости процесса нам нужен был инструмент, который позволяет более глубоко понять, как исполняется наш код. На эту роль отлично подходят семплирующие профайлеры. Здесь нам несказанно повезло, и ровно в момент, когда нам это потребовалось, в WAMR добавили такую возможность.
Проведя первичный анализ исполнения и улучшение производительности с использованием flamegraph как в части стандартной библиотеки, так и кодогенерации, мы получили цифры, которые всё ещё уступали старому исполнителю на базе стековой машины. Здесь мы решили понять, а насколько код собираемый в Wasm вообще быстро работает и как можно сравнить его производительность с нативным ассемблером. Возможность собирать нативные бинарные сборки позволила нам получить следующие виды исполняемых файлов:
ELF 32-bit;
ELF 64-bit;
WebAssembly (wasm) binary module.
Так как Wasm мы собираем в режиме wasm32, то было решено сперва сравнить этот исполнитель с нативным 32 битным ELF форматом. Разница для консольной утилиты составила примерно 20% по итоговым числам EPS. Глубокое изучение документации по средам исполнения Wasm привело меня к технике SEGUE. Эта аппаратная технология в современных процессорах x86 позволяет практически полностью нивелировать косвенную работу с относительными адресами в процессе выполнения. В новых поколениях процессоров появился регистр, куда кладётся базовый адрес Wasm региона. Подробнее читайте по ссылке.
После активации поддержки этого функционала разница между нативным 32 bit ELF и WASM кодом составила примерно 5–7% по EPS. Что в целом показалось хорошим результатом. Разница же между ELF 64-bit и ELS 32-bit составляла уже 35%. Те Wasm уступал примерно 40% обычно 64 битному коду. Грусть этих цифр заключалась в том, что в этой точке компилятор формулы в 64 bit ELF уже обгонял по производительности текущий стековый исполнитель XP и было примерно понятно, что нужно ускорить, чтобы получить прирост ещё в 50%.
Здесь мы детально сравнили flame графы для всех трёх режимов. Приложу сюда SVG файл для ELF 64-bit. Другие режимы отличались существенным увеличением времени работы кода библиотек simdjson|simdutf, а именно они сваливались в FALLBACK реализацию без использования SIMD инструкций.
Беглое изучение исходных текстов этих библиотек показало, что для ELF 32 bit никакой поддержки SIMD не предусмотрено, и также отдельная поддержка WASM_SIMD128 отсутствует из коробки. Для детального анализа влияния SIMD на производительность были собраны и протестированы следующие дополнительные исполняемые файлы на основании списка наборов инструкций поддерживаемых используемыми библиотеками:
ELF 64-bit (FALLBACK);
ELF 64-bit SSE4.2 (WESTMERE);
ELF 64-bit AVX2 (HASWELL).
В нашем сценарии использования разница между FALLBACK и WESTMERE получалась на уровне 25%. Разница между WESTMERE и HASWELL получилась на уровне 3–5%. Теперь стало понятно, откуда такая большая разница между бинарными файлами ELF 64-bit и ELS 32-bit. По умолчанию библиотека SIMDUTF динамически выбирает таблицу функций на основании используемой архитектуры через CPUID. В случае рабочих ноутбуков разработчиков это был как раз haswell_backend
.
32 битная ELF сборка не использует SIMD и этим фактом сразу уступает на 25–30%. Ещё примерно 10–15% разница между кодом стандартной библиотеки С/С++ в 32 и 64 битном режиме — новые доработки и оптимизации в основном делаются для 64 бит. А так же ряда технических отличий по типу количества регистров — в 64 битном режиме их больше и это развязывает руки компилятору. Подробнее это можно прочитать в книге, раздел: 2.3 Choice of operating system.
Дальше мы начали исследовать, чего стоит добавить поддержку simd (заголовочный файл: wasm_simd128.h
) в библиотеки simdjson|simdutf. Но, к сожалению, из‑за недостатка времени и подобного опыта у членов команды эти работы были свёрнуты. По ходу работы с simd возникла ещё одна проблема. Режим интерпретации в WAMR, который мы использовали для прогона тестов на CI не поддерживает simd128
. С ним умеет работать только JIT или AOT исполнитель, а их использование существенно замедляло прогон CI.
В случае если вы хотите получить максимум производительности от своего приложения, вам стоит озаботиться изучением поддержки simd в тех средах, в которых вы хотите запускать своё приложение. Беглый поиск на просторах сети показал, что многие среды не умеют в simd или умеют с ограничениями и требуют дополнительных флагов в конфигурации для активации поддержки. Также стоит быть готовым к тому, что вам придётся писать использование simd128
руками в свой код или вносить патчи в сторонние библиотеки, используемые в вашем проекте. По моим грубым оценкам, набор инструкций и уровень производительности wasm_simd128
будет примерно соответствовать WESTMERE SSE4.2.
Параллельно с исследованием проблемы SIMD проводились тесты серверного продукта после встраивания новой SDK. Здесь возникла архитектурная проблема следующего рода: Wasm умеет работать только с плоскими данными, скопированными в его кучу. Текущая стековая машина XP оперировала в общем с хостом адресном пространстве. За время существования серверный продукт оброс некоторым функционалом на входе и выходе с проверками событий. С учётом ограниченного бюджета времени на переделки мы были вынуждены выполнять декодирование и кодирование событий как внутри внутри Wasm, так и в нативном коде, чтобы не трогать текущий алгоритм валидации данных.
Это решение добавило пенальти примерно 20% от итогового числа EPS. Такая просадка лишь косвенно связана с Wasm машиной. Но при встраивании в реальный продукт вам придётся бороться с тем, чтобы избегать дополнительных преобразований с входящими и исходящими данными вне Wasm мира, если производительность критична для вашего решения. Хорошим подспорьем здесь выглядят плоские форматы с индексом позволяющие быстро проверить пару значений на уровне хост кода и потом скопировать весь блок в Wasm для полного DOM разбора и детальной обработки.
Приложу сюда для истории один из последних выводов perf stat на запуске консольного бенчмарка для JSON решателя:
Performance counter stats for '~/gits/evt.runtime/build_host/bin/wamr_cli -j ~/gits/evt.runtime/build_wasm/bin/xp_module.aot -b -r ~/etalons/json_full.txt':
48.283,63 msec task-clock # 1,000 CPUs utilized
212 context-switches # 4,391 /sec
10 cpu-migrations # 0,207 /sec
17.768 page-faults # 367,992 /sec
201.047.930.123 cycles # 4,164 GHz (83,33%)
818.687.604 stalled-cycles-frontend # 0,41% frontend cycles idle (83,33%)
2.898.441.711 stalled-cycles-backend # 1,44% backend cycles idle (83,34%)
514.123.678.827 instructions # 2,56 insn per cycle
# 0,01 stalled cycles per insn (83,33%)
89.466.635.106 branches # 1,853 G/sec (83,33%)
1.235.199.118 branch-misses # 1,38% of all branches (83,33%)
48,290357235 seconds time elapsed
46,955854000 seconds user
1,327882000 seconds sys
В этой точке времени у нас подходил к концу наш цикл R&D, и нам предстояло сделать выводы и перетащить максимум пользы здесь и сейчас в основной продукт.
Разочарование
Здесь хотелось бы перечислить список основных разочарований от использования Wasm:
сырость средств разработки для экосистемы (при профилировании мы нашли ошибку в demangling символов на
wasm_imported_functions
,llvm‑objdump
из пакета компилятора не умеет понимать какая функция вызывается при использовании дизассемблера, нужно использовать только утилиты из пакета wabt определённой версии).производительность получилась не такой высокой, как хотелось, из‑за тонкостей с SIMD и необходимости изменения архитектуры кода приложения.
Да, средства разработки Wasm развиваются и с каждым днём становятся всё лучше. Да, производительность может быть на 15–20% медленнее, чем у нативного приложения ELF 64-bit SSE4.2, при условии использования WASM_SIMD128 и очень аккуратного встраивания модуля в архитектуру вашего приложения. Что на самом деле выдающееся достижение для переносимого байт кода, задача которого запускаться на максимально широком наборе устройств от IOT, смартфона или большого севера.
Но это будет при условии использования самой долгой компиляции Wasm → Native. У нас эти цифры достигались в режиме AOT компиляции запускаемого модуля и при наличии аппаратной поддержки SEGUE на аппаратуре запуска. Без этого цифры даже на LLVM исполнителях будут скромнее: на уровне минус 30% и ниже. Альтернативные среды исполнения Wasm, например Cranelift, по скорости из их документации уступают LLVM‑based исполнителям примерно на 15%.
Отдельным блоком идут проблемы с длительным временем запуска и большим объёмом памяти при загрузке крупных исполняемых Wasm файлов в случае использования JIT|AOT компиляции LLVM‑based исполнителей. При транслировании Wasm кода в исполняемый в момент запуска у вас есть вилка: вы платите огромную цену за оптимизацию кода или получаете существенное пенальти по производительности во время исполнения.
По нашим оценкам время AOT трансляция может отличаться на порядок для O0 и O3 уровней оптимизации. По нашим экспериментам Wasm в связке с LLVM будет отлично себя показывать на небольших примерно в 5 мегабайт или хорошо на средних объёмах исполняемого кода примерно в 35 мегабайт. В LLVM исполнителях один Wasm модуль представляет собой монолитный LLVM юнит компиляции со всеми вытекающими проблемами. На больших модулях в 200 мегабайт Wasm кода требовались десятки минут и гигабайт оперативной памяти в момент старта в случае высокого уровня оптимизации.
При использовании Wasm преследовало ощущение, что это ещё достаточно сырая технология. Проблемы с Wasm, будут знакомы тем, кто пытался выполнить портирование уже работающего реального продукта под редкую программно‑аппаратную платформу. Также нужно быть готовым к ограничению в 4 Гб памяти на один экземпляр Wasm машины при использовании wasm32
.
На текущий момент wasm64
— слишком экспериментальный зверь как минимум для С++ разработки. Такое ограничение на объём памяти, например, не позволит вам хранить огромные хеш‑таблицы для кеширования данных. Возможно для Rust или Go тулчейнов проблемы wasm32
не существует — мы такую работу не проводили. Но остаётся вопрос поддержки wasm64
в средах исполнения и наличие аппаратного ускорения для ускорения работы с памятью.
Послесловие
WebAssembly интересная и перспективная технология. Она имеет много сторонников и развивается большим сообществом коммерческих разработчиков и энтузиастов каждый день. Определённо, у неё есть своя ниша использования. Основным преимуществом будет доступ к широкому набору устройств с возможностью запуска вашего Wasm кода.
Безопасность исполняемого кода и возможность гибкой конфигурации API доступного приложению здесь возведены в абсолют. Но эта технология также имеет массу ограничений как по средствам разработки, так и с доступными библиотеками, а также нюансы с итоговой производительностью кода. Она однозначно не является серебряной пулей в сегменте генерации инструкций по выполнению пользовательской бизнес логики.
Это было интересное погружение в мир WebAssembly со своими опасностями и глубоко лежащими жемчужинами, но пришло время всплывать. Наши следующие шаги по улучшению исполнителя XP заслуживают отдельной статьи, но это уже совсем другая история.
Всем хорошего дня, спасибо за внимание.
Если хотите разрабатывать компиляторы предметно‑ориентированных языков программирования и стековые машины — приходите к нам работать.
Комментарии (11)
slonopotamus
26.09.2024 09:45+10Я правильно понял что вы крутите свой собственный код на своих же серверах внутри WebAssembly-машины? Ааа... ммм... А зачем? Что не так с нативными бинарями?
Ну то есть я понимаю смысл WebAssembly-песочницы в браузере, там нужно чужой недоверенный код выполнять. Но в вашем-то случае какую проблему оно решает?
proydakov Автор
26.09.2024 09:45+2Отчасти так, но как обычно дьявол кроется в деталях. Проблема состоит в следующем. У нас есть доверенный код среды исполнения. Этот код проходит сертификацию. Далее исполняемые файлы поставляется клиентам где они будут работать на их оборудовании.
Система для работы использует большое количество правил экспертизы, которую пишут как сотрудники компании, так и сторонние организации. При желании клиент может в панели администратора добавить набор правил под своё оборудование если возникает такая потребность. Также клиенты получают патчи с базой обновлённых правил через определённые интервалы времени. В случае критически проблем с безопасностью это считанные дни. Поэтому к сожалению мы не можем собрать один раз исполняемые файлы, проверить их на закладки и потом использовать.
Для решения этой задачи компания разрабатывает спецификацию языка XP, формат представления AST, компилятор, стандартную библиотеку и среду исполнения. Собственно с помощью WebAssembly предполагалось заменить AST и часть стека исполнения.
Alexufo
26.09.2024 09:45Если есть доверенный код, в чем же проблема компилить бинарник под требуемую архитектуру? Код же не становится другим.
Да вы вообще ещё в это приплетаете чужой код ( в виде среды исполнения) и зависите от него. Он каким образом прошел сертификацию? А если бы не прошел? А если завтра перестанет проходить?
ristle
26.09.2024 09:45+1Добрый день!
Спасибо из интересную статью, особенно про wasm) Правильно ли я понимаю, что был взят только C++ для Wasm? И их каких побуждений? Было ли это продиктовано запуском и проверкой чужого кода внутри wasm?
Сам тыкал wasm, правда только на Rust , где он показался крайне удобной и приятным инструментом для разработки
tbl
26.09.2024 09:45Сам тыкал wasm, правда только на Rust , где он показался крайне удобной и приятным инструментом для разработки
Как с дебагом и юнит-тестами обстоят дела? У меня получалось тестировать и дебажить приложение, скомпилированное в нативный код, а wasm32-сборку только руками проверять, что там все ок
tbl
26.09.2024 09:45Есть wasm_bindgen_test, но он со своими макросами и ограничениями (например, можно тестировать только pub-модули, достижимые из корня крейта) выглядит чужеродным костылем в экосистеме раста
Ну и вишенка на торте: https://rustwasm.github.io/book/reference/debugging.html#avoid-the-need-to-debug-webassembly-in-the-first-place
ristle
26.09.2024 09:45Конечно не очень комфортно в rust для тестировать все в публичные крейты писать, но думаю это решиться в ближайшее время, учитывая возросший интерес к Wasm. Пробовал только в личных проектах, поэтому было некоторые допущения к коду проекта))
Сейчас даже не так сложно написать wasm приложение на Android как это было около года назад. Тут хочу оставить ремарку, что речь о Rust проектах, где используется один код для вебсайта, десктопного приложения и мобильного
proydakov Автор
26.09.2024 09:45+5Добрый день.
Наш язык XP имеет большое количество встроенных функций. Сейчас их около сотни. На текущий момент эти функции реализованы на языке С++. В их разработку, отладку, тестирование, документирование вложено много человеко-лет труда. Собственно, идея была по максимуму использовать существующий код.Также, пока мы работали на WebAssembly реализацией, работа над основным продуктом не останавливалась. Было бы крайне тяжело всё это переписать на другой язык программирования и поддерживать одновременно две версии.
Сторонний код, в отличие от стандартной библиотеки, пишется на XP. Для него был реализован отдельный компилятор в Wasm. Но это тема для отдельной статьи.
qrdl
Из каких соображений был выбран WAMR, а не wasmtime, например?
proydakov Автор
Мы пробовали четыре машинки: wamr, wasmtime, wasmedge, v8. На начальном этапе wamr с AOT компиляцией показал самую высокую производительность на базовых примерах.
Также её удалось очень быстро встроить в приложение и pipeline разработки. Плюс разработчики довольно оперативно реагировали на наши вопросы в github трекере. Поэтому решили начать с неё, а потом уже было сложно поменять.
Наверное, было бы интересно сравнить итоговый продукт на каждой из сред. Но, к сожалению, на это недостаточно времени и ресурсов.