WebAssembly активно разрабатывается и уже достиг состояния, когда собранный модуль можно попробовать в Chrome Canary и Firefox Nightly, включив флажок в настройках. Сравним производительность арифметических вычислений с 64-битными числами в WebAssembly, asm.js, PNaCl и native-коде. Посмотрим на инструменты, которые есть для WebAssembly сейчас, и заглянем в недалёкое будущее. WebAssembly сейчас в стадии разработки, уже через месяц статья может устареть. Цель статьи — рассказать о положении дел, для тех, кто интересуется. демо, графики В качестве бенчмарка возьмём Argon2, который мне недавно понадобилось вычислять в браузере.
Argon2 (github, хабр) — это сравнительно новая криптографическая функция формирования ключа (key derivation function, KDF), победившая на Password Hashing Competition. Она основана на 64-битной арифметике, вот функции, за одну итерацию выполняющиеся около 60M раз: Казалось бы, всё просто: взять да перемножить два 64-битных числа, как это и сделано в native-коде argon2, например, вызвав sse-инструкцию. Но только не в браузере. В V8, как известно, нет 64-битных integer-ов, поэтому вся арифметика за неимением лучшего эмулируется 31-битными smi (small integer). Что очень медленно. Настолько медленно и настолько безобразно, что это неоднократно упоминали разработчики Unity, и 64-битные типы включили в WebAssembly MVP, хотя сначала хотели отложить на потом. Посмотрим на код, сгенерированный asm.js для функции умножения двух int64, это функция из compiler-rt: Вот она, и вот она же на JavaScript. Реализация кстати очень хорошая, не вызывающая deopt-ов в V8. Проверим это на всякий случай: Скомпилируем asm.js, отключив переименование переменных, чтобы имена функций в коде были читабельными, и запустим с флагами, позволяющими открыть артефакты в IR Hydra (можно просто установить npm i -g node-irhydra): Как видим, V8 даже заинлайнил функцию __muldsi3 в __muldi3. Там же можно посмотреть на ассемблерный код этой функции. 140 andl r8,0xffff
;; <@43,#72> gap
147 movq r9,rdx
;; <@44,#76> bit-i
150 andl r9,0xffff
;; <@48,#79> load-context-slot
170 movq r11,[r11+0x77]
;; <@50,#82> check-value
174 movq r10,0x3d78a90c3b59 ;; object: 0x3d78a90c3b59 <JS Function imul (SharedFunctionInfo 0x3d78a9058f91)>
184 cmpq r11,r10
187 jnz 968
;; <@51,#82> gap
193 movq rdi,r9
;; <@52,#83> mul-i
196 imull rdi,r8
;; <@53,#83> gap
200 movq r11,rax
;; <@54,#89> shift-i
203 shrl r11, 16
;; <@55,#89> gap
207 movq r12,rdi
;; <@56,#94> shift-i
210 shrl r12, 16
;; <@58,#100> mul-i
214 imull r9,r11
;; <@60,#104> add-i
218 addl r9,r12
;; <@61,#104> gap
221 movq r12,rdx
;; <@62,#111> shift-i
224 shrl r12, 16
;; <@63,#111> gap
228 movq r14,r12
;; <@64,#118> mul-i
231 imull r14,r8
;; <@65,#118> gap
235 movq r8,r9
;; <@66,#125> shift-i
238 shrl r8, 16
;; <@68,#131> mul-i
242 imull r12,r11
;; <@70,#135> add-i
246 addl r12,r8
;; <@71,#135> gap
249 movq r8,r9
;; <@72,#142> bit-i
252 andl r8,0xffff
;; <@74,#145> add-i
259 addl r8,r14
;; <@76,#151> shift-i
262 shrl r8, 16
;; <@78,#153> add-i
266 addl r8,r12
;; <@80,#238> smi-tag
269 movl r12,r8
272 shlq r12, 32
;; <@84,#158> store-context-slot
289 movq [r11+0x6f],r12
;; <@86,#163> add-i
293 leal r8,[r9+r14*1]
;; <@88,#166> shift-i
297 shll r8, 16
;; <@90,#170> bit-i
301 andl rdi,0xffff
;; <@92,#172> bit-i
307 orl rdi,r8 Chrome генерирует не такой оптимальный код, как можно было бы сделать, имея аннотации типов, разработчики V8 принципиально не хотят поддерживать asm.js js subset, и вобщем-то, правильно. В отличие от этого Firefox, видя "use asm", в случае если код проходит валидацию, выкидывает некоторые проверки, в результате чего получившийся код быстрее где-то в 3..4 раза. По сравнению с native-кодом, Chrome и Safari медленнее в 50 раз, Firefox — в 12.
IE11 достаточно медленный, а вот Edge с включённым в настройках asm.js где-то посередине между Chrome и Firefox: Скомпилируем этот код в WebAssembly. Сделать это можно несколькими способами, для начала попробуем C/C++ Source ⇒ asm2wasm ⇒ WebAssembly (некоторые параметры исключены для краткости): Можно использовать тот же toolchain, что и для сборки asm.js, указав, что мы хотим использовать binaryen (-s BINARYEN=1).
На выходе мы получим: Преобразуем wast-файл в бинарный формат wasm: Что же умеет делать js-обёртка, кроме вызова wasm-модуля, и как её использовать? Здесь мы указваем на расположение скомпилированных артефактов, подключаем свои логи и определяем метод, которым будем выполнять код. Из методов можно выбрать: Можно перечислить несколько методов через запятую, тогда будет выполнен первый успешный из них. По умолчанию берётся метод native-wasm,interpret-binary, то есть попробовать, нет ли wasm, если нет, то интерпретировать бинарный модуль. После успешной загрузки в объекте Module появляются все экспортированные методы. Пример использования (полностью): Firefox Nightly позволяет заглянуть внутрь wasm-модуля: 
В Chrome пока нет инструментов для просмотра wasm, модуль даже не отображается в редакторе. Но к релизу тоже обещают сделать view source. Binaryen генерирует интерпретатор, который умеет выполнять текстовый .wast и бинарный .wasm форматы. Попробовать его можно, выставив method в interpret-s-expr или interpret-binary. Пока что интерпретатор настолько медленный, что подсчёта хэша я не дождался, а оценил по меньшему числу итераций. Оно составило бы полчаса, в то время как в Chrome было 7 сек и даже в IE11 45 сек. Посмотрим, какой же код у нас получился. Я написал простой тест, чтобы .wast был предельно маленьким, вот он: Заглянем в .wast и найдём нашу функцию: Снова i32? Почему так получается? Вспомним, что мы получили wasm-код, скомпилировав asm.js, поэтому i64 мы здесь и не увидим. Неудивительно, что такой код тоже выполняется долго. Однако же, теперь скорость выполнения в Chrome получилась такой же, как в Firefox, и чуть быстрее, чем asm.js в Firefox.  Теперь попробуем более сложный способ, C/C++ Source ⇒ WebAssembly LLVM backend ⇒ s2wasm ⇒ WebAssembly. LLVM научился генерировать WebAssembly, делая это без emscripten. Но делает он это пока очень плохо, получившийся модуль не всегда работает. Собираем LLVM с поддержкой WebAssembly: Включаем её в компиляции: Чтобы попробовать разные версии LLVM в emscripten, указаываем путь к нему в ~/.emscripten, LLVM_ROOT. И… получаем ошибку при загрузке модуля в браузер. Можно ещё собрать не форком fastcomp, используемом в emcc, а ванильным LLVM из апстрима, как-то так: Тоже падает. Возможно, wasm из wast для V8 надо собирать sexpr-wasm-prototype, а не binaryen, но это всё равно не помогает. Однако же простой тест вполне себе работает, можно хотя бы оценить производительность на примере одной функции. Посмотрим на получившийся .wast: Ура, i64! Загрузим его в браузер и оценим время, в сравнении с прошлым вариантом: В светлом будущем скорость 64-битной арифметики лучше в несколько раз. В MVP WebAssembly не включены pthreads, они появятся только потом. Пока сложно сказать, что будет, вобщем, на ближайший год — ответ нет. Но зато WebAssembly можно без проблем использовать в web worker-ах без какой-либо деградации производительности, в чём вы можете убедиться сами на демо-страничке. Теперь сравним производительность с PNaCl. PNaCl — это тоже формат бинарного кода, разработанный в Google для Chrome и даже включённый по умолчанию. Когда-то предполагалось поддержать его и в других браузерах, но Mozilla отвергла предложение, а другие и не задумывались. Не взлетело. Итак, PNaCl — это .pexe, который jit-ится в рантайме, сделаем простой модуль для него: Вызвать это можно, внедрив .pexe на страничку в <embed> и отправив ему сообщение: В отличие от WASM, в PNaCl уже сейчас поддерживает и 64-битные типы и pthreads, поэтому работает намного быстрее, по скорости, время работы в 1.5..2 раза больше native-кода. Но это только хром. Грустно тут только время загрузки, которое составляет несколько секунд, а в случае вообще первого использования PNaCl пользователем может вырасти до невменяемых цифр порядка 30 сек. Среднее время выполнения кода в разных средах: Время загрузки и первого выполнения: В node.js скомпилировать native-код очень просто уже сейчас, достаточно добавить пару binding-ов. Когда V8 обновится до какой-то версии, node.js можно будет запустить с флагом --expose-wasm (пока его поддержка в экспериментальной стадии) и использовать wasm и в ноде. Пока он не загружается, потому что V8 достаточно старый. Сейчас разумно использовать asm.js в Firefox и PNaCl в Chrome. WASM уже сейчас достаточно хорош, ко времени MVP, компиляцию в LLVM, скорее всего, доведут до ума, но использовать его, конечно же, рано, даже в nightly-билдах по умолчанию он выключен. Однако производительность wasm уже сейчас показательна и превышает скорость работы asm.js, даже без поддержки i64. Web engineer