Download WebAssembly article PDF [PDF 263 KB]
Введение
Эта публикация — первая в серии статей, в которых дается общее представление о WebAssembly (wasm). Этот язык еще не полностью доработан, ожидается немало изменений, но в этой статье вы получите общее представление о текущем состоянии wasm. Мы будем выпускать дальнейшие статьи с описанием изменений по мере их появления.
Цель wasm состоит в повышении производительности JavaScript *. Этот язык определяет новый портативный формат файлов, высокоэффективный с точки зрения размера и времени загрузки, пригодный для компиляции данных для веб-страниц. Он будет использовать существующие веб-API и должен стать составной частью набора веб-технологий. Хотя название WebAssembly содержит приставку веб-, этот язык предназначен не только для браузеров, он должен предоставлять такие же возможности и для использования в других целях. За счет этого wasm может получить более широкое распространение.
Как и во всех языках с динамической типизацией, в JavaScript крайне трудно добиться оптимальной производительности работы, поэтому в течение многих лет на веб-страницах применялась возможность использовать машинный код (C/C++). Альтернативные языки, такие как NaCl и PNaCl, работают на веб-страницах вместе с JavaScript. Наибольшую практическую отдачу обеспечивает asm.js — это подмножество JavaScript с компонентами, при компиляции которых достигается производительность почти на уровне машинного кода. Но ни одно из этих альтернативных решений не получило повсеместного распространения на веб-страницах для повышения производительности и использования кода. Эту проблему и призван решить язык wasm.
Wasm должен не заменить JavaScript, а предоставить возможность достижения производительности на уровне машинного кода для основных частей приложения (это могут быть как веб-приложение, так и обычное приложение). По этой причине разработчики браузеров, веб-приложений, компиляторов и прочего ПО вместе работают над определением этой технологии. Одна из целей состоит в определении нового двоичного формата кода, независимого от платформ, для веб-страниц.
Если для выполнения кода на веб-страницах удастся добиться производительности, близкой к скорости машинного кода, это может произвести настоящую революцию в появлении новых возможностей в веб-решениях. В машинном коде новые функции зачастую предоставляются в пакетах SDK, для чего применяются соответствующие библиотеки. Веб-страницы не имеют доступа к этим библиотекам по соображениям безопасности. Для реализации поддержки новых возможностей зачастую требуются сложные стандартизованные API в составе веб-браузеров, чтобы устранять сопутствующие проблемы. К примеру, библиотеки JavaScript работают слишком медленно.
При использовании wasm эти стандартные API могут стать намного проще и работать на более низком уровне. При этом библиотеки динамической компоновки wasm, поддерживающие кэширование, многопоточность и обработку множества данных одной инструкцией (SIMD), обеспечат функциональность, недостижимую сегодня. Например, вместо сложных стандартных API для распознавания лиц или построения трехмерного изображения для использования с трехмерными камерами упрощенный стандартизованный API сможет просто предоставлять доступ к потоку необработанных трехмерных данных. Модуль wasm при этом будет осуществлять обработку, которая сейчас осуществляется библиотекой SDK. Это даст возможность загружать и кэшировать распространенные библиотеки динамической компоновки и быстро получать доступ к новым возможностям из Интернета, намного раньше возможного завершения процесса стандартизации.
В данной статье предлагается введение в эту стремительно развивающуюся технологию. В статье приводится общее описание, поскольку многое в спецификации wasm пока еще находится на уровне разработки.
Дополнительные сведения об общих целях и задачах wasm см. здесь:
https://github.com/WebAssembly/design/blob/master/HighLevelGoals.md5.
Общее описание
Проект и спецификация
Первоначальные документы проекта wasm находятся в следующем хранилище, в котором, в частности, есть следующие файлы.
- AstSemantics.md— содержит описание самого формата.
- MVP.md— определение «минимального жизнеспособного продукта» — требования для первой итерации wasm.
- HighLevelGoals.md— общие цели wasm и сценарии, для которых предназначен этот язык.
Для точного определения и проверки решений, записанных в проектный документ, в хранилище specнаходится интерпретатор OCaml языка wasm. Также содержится папка тестового пакета с несколькими начальным тестами wasm. Набор тестов включает различные тесты — от общих вычислений с целыми числами и числами с плавающей запятой до операций с памятью.
Еще несколько инструментов в основном хранилище wasm в github используют этот же набор тестов для регрессивного тестирования. Например, это wasm-to-llvm-prototypeи wasmint.
Прототипы
Базовая страница wasm в githubсодержит различные действующие проекты. Некоторые из них упоминаются в этой статье, но достойны внимания и многие другие. Читателю рекомендуется изучить различные проекты, чтобы увидеть, на что сообщество wasm направляет свои усилия, но все хранилища можно разделить на пять основных групп.
- Проекти спецификация: каноническое определение wasm.
- binaryen: из C/C++ в wasm, иногда что-нибудь еще.
- sexpr-wasm-prototype: из wasm в двоичный формат.
- wasm-to-llvm-prototype, wasm-jit-prototype, wasmint: из wasm в исполняемый код.
- polyfill-prototype-2: из JavaScript в wasm.
Многие ресурсы в этих хранилищах являются проверочными, т. е. их цель — опробовать что-либо в действии и набрать опыт, они далеко не всегда представляют конечный результат. Инженеры, работающие с хранилищами, часто экспериментируют с wasm, чтобы понять, как все работает. Например, это касается различных тестируемых двоичных форматов, таких как polyfill-prototype-2или двоичный формат v8format.
WebAssembly: первый взгляд
Модули, более крупные составные части
В wasm модули являются наиболее крупными компонентами. Внутри модулей находятся функции и запросы выделения памяти. Модуль wasm — это совокупность распространяемого исполняемого кода. Каждый модуль обладает собственным линейным пространством памяти, функциями импорта и экспорта, а также кодом. Модуль может быть исполняемым файлом, библиотекой динамической компоновки (в будущих версиях wasm) или кодом для выполнения на веб-странице (используется в случаях, когда можно использовать модули ECMAScript 6 *).
Действующие тестовые файлы в тестовом пакете позволяют определять несколько модулей в одном файле, но считается, что в итоговой версии это будет не так. Напротив, чаще всего целая программа будет образовывать один большой модуль. Поэтому большинство программ C/C++ будут преобразованы в одиночные модули wasm.
Функции
В wasm применяется статическая типизация с возвратом; все параметры типизируются. Например, в этой строке из файла i32.wast в хранилище тестового пакетапоказано добавление двух параметров.
(func $add (param $x i32) (param $y i32) (result i32) (i32.add (get_local $x)
(get_local $y)))
Все они являются 32-разрядными целочисленными значениями. Строка выглядит следующим образом.
- Объявление функции с именем $add.
- Она содержит два параметра, $x и $y, оба — 32-разрядные целочисленные значения.
- Результат — 32-разрядное целочисленное значение.
- Тело функции — 32-разрядное сложение.
- Левая сторона — значение в локальной переменной или параметре $x.
- Правая сторона — значение в локальной переменной или параметре $y.
- Поскольку нет явного узла возврата, возвращается последняя инструкция функции, то есть сложение.
Более подробные сведения о функциях и коде wasm см. ниже в этой статье.
Сходство с AST
Согласно общему определению wasm классифицируется как дерево абстрактного синтаксиса (AST), но также обладает некоторыми механизмами управления потоком и определением локальных переменных, что позволяет обрабатывать временные расчеты. Текущим текстовым форматом в wasm являются С-выражения (символьные выражения), хотя принято решение о том, что в итоговой версии wasm текст будет представлен иначе.
Тем не менее для пояснения работы wasm в этом документе они вполне пригодны. За исключением общих конструкций потока управления, таких как условия if, циклы и блоки, вычисления в wasm имеют формат AST. При этом для следующего вычисления:
(3 * x + x) * (3 * x + x)
используется следующий код wasm.
You could see the following wasm code:
(i32.mul (i32.add (i32.mul (i32.const 3) (get_local 0)) (get_local 0)) (i32.add (i32.mul (i32.const 3) (get_local 0)) (get_local 0)) )
Это означает, что компилятору из кода wasm в машинный код придется исключать распространенные подчиненные выражения для достижения высокой производительности. Для решения этой проблемы wasm позволяет коду использовать локальные переменные для хранения временных результатов.
Наш пример превращается в следующий код.
(set_local 1 (i32.add (i32.mul (i32.const 3) (get_local 0)) (get_local 0)) ) (i32.mul (get_local 1) (get_local 1) )
Ведутся обсуждения того, где следует производить оптимизацию:
- между исходным кодом, например C/C++, и wasm;
- между wasm и двоичным кодом, используемым для целевой архитектуры в браузере или в других приложениях.
Память
Подсистема памяти для wasm называется линейной памятью, где модуль может запросить заданный объем памяти, начиная с адреса 0. Для загрузки в память и для сохранения из памяти можно использовать либо константы для простых примеров кода, либо также переменные с адресами.
Например, следующий код сохраняет целое число 42 по адресу 0.
(i32.store (i32.const 0) (i32.const 42))
Wasm определяет знак и нулевое расширение для операций с памятью. Операции также могут определять выравнивание операций с памятью, если это позволит создать более эффективный код для заданной архитектуры. И наконец, операция также обладает параметром смещения, чтобы дать возможность загрузить (к примеру) поле структуры.
Прототип wasm— LLVM
Прототип wasm — LLVM, инструмент, который я разрабатываю для wasm, предоставляет возможность компилировать код wasm непосредственно в код x86 посредством компилятора LLVM. Изначально предполагалось использовать wasm в веб-браузерах, но есть планы использовать wasm и в других сценариях, не связанных с браузерами.
Общая структура прототипа wasm — LLVM состоит в разборе тестового файла wasm с помощью программ с открытым исходным кодом flex и bison и в построении промежуточного представления. В этом промежуточном представлении есть этап (а в будущем, вероятно, их будет несколько), на котором производится промежуточная обработка перед формированием кода с помощью компилятора LLVM.
Рисунок 1.Прототип wasm— LLVM
На рис. 1 показана базовая структура этой программы: в качестве начальных входных данных используется временный текстовый формат wasm. Такой формат называется С-выражениями. С-выражение проходит анализ лексическим и синтаксическим анализаторами, реализованными с помощью средств flex и bison. После анализа С-выражения создается внутреннее промежуточное представление, затем осуществляется промежуточная обработка перед получением внутреннего представления LLVM. В промежуточном представлении LLVM код отправляется в оптимизатор LLVM, после чего создается код Intel® x86.
Первый пример на wasm
Здесь приводится простой пример: суммирование всех значений массива. Этот пример показывает, что освоить wasm довольно просто, следует лишь помнить о нескольких особенностях.
Предполагается, что код wasm будет создаваться компилятором, а исходным языком для этого будет C/C++. Не существует отдельного компилятора, который преобразует код C/C++ в wasm, хотя в LLVM уже ведутся работы, связанные с wasm. По всей видимости, и другие компиляторы, такие как GCC и MSVC, будут поддерживать язык и среду wasm. Написание непосредственно кода wasm вряд ли будет распространено, но интересно посмотреть на такой код и понять, как будет устроено взаимодействие языка с браузером/ОС и с архитектурой системы.
Сумма массива
;; Initialization of the two local variables is done as before: ;; local 1 is the sum variable initialized to 0 ;; local 2 is the induction variable and is set to the ;; max element and is decremented per iteration (loop (if_else (i32.eq (get_local 2) (i32.const 0)) (br 1) (block (set_local 1 (i32.add (get_local 1) (i32.load (get_local 2)))) (set_local 2 (i32.sub (get_local 2) (i32.const 4))) ) ) (br 0) )
Примечание. Описываемый подход не всегда является оптимальным. Он используется здесь для демонстрации основных компонентов языка wasmи его логики. В другой статье будут описаны различные конструкции, способные лучше имитировать инструкции используемого оборудования.
В приведенном выше примере цикл определяется узлом цикла. Узлы циклов могут определять имена блоков начала и выхода или могут быть анонимными, как здесь. Для лучшего понимания кода в цикле используются две локальные переменные. Local 1 — переменная суммы при обходе массива, а local 2 — обновляемая индукционная переменная. Local 2 фактически представляет указатель на текущую суммируемую ячейку.
Вот аналогичный код на C.
// Initialization is done before // local_1 is the sum variable initialized to 0 // local_2 is the induction variable and is set to // the max element and is decremented per iteration do { if (local_2 == start) { break; } local_1 = local_1 + *local_2; local_2--; } while(1);
Циклы в wasm работают как конструкции do-while в C. Но неявное замыкание цикла отсутствует: в конце цикла wasm необходимо определить явную ветвь. После этого узел «br 0» в конце дает команду на возврат к верхнему узлу цикла: 0 представляет уровень вложенности цикла, на который нужно перейти.
Цикл начинает работу с проверки того, нужна ли дополнительная итерация. Если да, то выполняется «br 1», т. е. выход из начала цикла на один уровень. В этом случае, поскольку существует только один уровень, мы выходим из цикла.
В цикле код использует постепенно уменьшающийся указатель для достижения начала массива. В версии на C существует удобная переменная, названная start. Она представляет массив, суммирование которого выполняет код.
В wasm, поскольку разметка памяти начинается с адреса 0, а данный массив является единственным в этом до крайности упрощенном примере, произвольным образом определено, что начальным адресом массива будет 0. Если поместить массив в какое-нибудь другое место, то это сравнение будет больше похоже на версию на языке С; локальная переменная 2 будет сравниваться со смещением, переданным с помощью параметра.
Обратите внимание на разницу в обработке обновления индукционной переменной между кодом C и кодом wasm. В коде C сам язык дает возможность программисту просто обновлять указатель на единицу, что далее в строке приведет к фактическому уменьшению на четыре. В wasm мы уже работаем на очень низком уровне, поэтому уменьшение на четыре происходит прямо здесь.
Наконец, как мы показали, (get_local 2) является счетчиком цикла, а (get_local 1) — фактической суммой вектора. Поскольку мы суммируем 32-разрядные значения, в операции используются коды операций i32.add и i32.load. В этом примере вектор, который мы суммируем, находится в начале линейной области памяти.
Создание кода wasm— LLVM
The wasm-to-llvm-prototype generates the following loop code quite easily:
.LBB4_2: movl %edi, %edx addl (%rdx,%rcx), %eax addl $-4, %edi jne .LBB4_2
Это весьма компактный цикл, если учесть первоначальный код wasm в текстовом формате. А вот эквивалентная версия на С.
for (i = 0; i < n; i++) { sum += tab[i]; }
Компилятор GNU * Compiler Collection (GCC) версии 4.7.3 выдает при уровне оптимизации -O2 следующий код с аналогичной логикой.
.L11: addl (%rdi,%rdx,4), %eax addq $1, %rdx cmpl %edx, %esi jg .L11
В случае с wasm мы получили цикл с обратным отсчетом и используем результат вычитания напрямую в качестве условия для перехода цикла. В случае с GCC требуется инструкция сравнения. Но в версии GCC не используется дополнительная инструкция перемещения, которая используется в wasm с LLVM.
При уровне оптимизации O3 компилятор GCC применил векторизацию цикла. Видно, что прототип wasm — LLVM необходимо доработать, чтобы он начал создавать оптимальный код (как с векторизацией, так и без нее). Этому будет посвящена другая статья в этой серии.
Заключение
В этой статье дается введение в язык wasm и показан простой пример суммирования массива. В дальнейших статьях будет дано более подробное описание wasm, будут описаны различные существующие инструменты для этого языка.
Wasm — новый язык, поэтому разрабатывается множество инструментов, чтобы помочь определить его потенциал, поддерживать его и понять, что этот язык может делать, а что — нет. Еще многие элементы, относящиеся к wasm, нуждаются в проработке, пояснении и изучении.
Об авторе
Жан-Кристоф Бейлер (Jean Christophe Beyler) — инженер по программному обеспечению в отделах Software and Solutions Group (SSG), Systems Technologies & Optimizations (STO) и Client Software Optimization (CSO) корпорации Intel. Он занимается компиляторами для Android и экосистемой Android в целом, а также другими технологиями, связанными с производительностью и компиляторами.