JavaScript: заметка о WebAssembly

Привет, друзья!
В 2019 году WebAssembly (далее — WA или wasm) стал четвертым «языком» веба. Первые три — это, разумеется, HTML, CSS и JavaScript. Сегодня wasm поддерживается 94% браузеров. Он, как утверждается, обеспечивает скорость выполнения кода, близкую к нативной (естественной, т.е. максимально возможной для браузера), позволяя портировать в веб десктопные приложения и видеоигры.
Что не так с JS?
JS — это интерпретируемый язык программирования с динамической типизацией. Динамическая типизация означает, что тип переменной проверяется (определяется) во время выполнения кода. И что с того? — спросите вы. Вот как определяется переменная в C++:
int n = 42
Такое определение сообщает компилятору тип переменной n и ее локацию в памяти. И все это в одной строке. А в случае с определением аналогичной переменной в JS (const n = 42), движку сначала приходится определять, что переменная является числом, затем, что число является целым и т.д. при каждом выполнении программы. На определение и (часто) приведение (преобразование) типов каждой инструкции уходит какое-то время.
Процесс выполнения кода в JS выглядит примерно так:
Разбор (парсинг) -> Компиляция и оптимизация -> Повторная (дополнительная) оптимизация или деоптимизация -> Выполнение -> Сборка мусора
А в WA так:
Расшифровка (декодирование) -> Компиляция и оптимизация -> Выполнение
Это делает WA более производительным, чем JS. В защиту JS можно сказать, что он разрабатывался для придания «легкой» интерактивности веб-страницам, а не для создания высокопроизводительных приложений, выполняющих сложные вычисления.
Что такое WA?
Формальное определение гласит, что WA — это открытый формат байт-кода, позволяющий переносить код, написанный на таких языках как C, C++, C#, Rust и Go в низкоуровневые ассемблерные инструкции, выполняемые браузером. По сути, это виртуальный микропроцессор, преобразующий высокоуровневый язык в машинный код.
На изображении ниже представлен процесс преобразования функции для сложения чисел (add), написанной на C++, в бинарный (двоичный) формат:
Обратите внимание: WA — это не язык программирования. Это технология (инструмент), позволяющая конвертировать код на указанных выше языках в понятный для браузеров машинный код.
Как WA работает?
WA — это веб-ассемблер. Но что такое ассемблер?
Если очень простыми словами, то
- Каждый процессор имеет определенную архитектуру, например,
x86илиARM. Процессор понимает только машинный код. - Писать машинный код, сами понимаете, сложно и утомительно. Для облегчения этого процесса существуют языки ассемблера.
- Ассемблер конвертирует инструкции на языке ассемблера в машинный код, понятный для процессора.
На изображении ниже представлен процесс выполнения программы на C на компьютере:
Пример использования WA
Код примера с исходниками.
Что нужно сделать, чтобы использовать WA в браузере (или на сервере в Node.js)? И действительно ли WA-код является более производительным, чем JS-код? Давайте это выясним.
Предположим, что у нас имеется такая функция на C++:
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
int ... int означает, что функция принимает целое число и возвращает целое число. Как видите, наша функция вычисляет сумму чисел из последовательности Фибоначчи (далее — фибонача :)).
Сначала эту функцию необходимо конвертировать в wasm-модуль. Для этого существуют разные способы и инструменты. В нашем случае для этого вполне подойдет WasmExplorer.
Вставляем код в первую колонку, нажимаем Compile для компиляции кода в Wat(текстовое представление двоичного формата wasm) и Download для преобразования .wat в .wasm и скачивания файла (test.wasm). Переименуем этот файл в fib.wasm.
Подготовим проект. Нам потребуется сервер. Зачем? Об этом чуть позже.
# создаем директорию и переходим в нее
mkdir wasm-test
cd wasm-test
# инициализируем Node.js-проект
yarn init -yp
# устанавливаем зависимости для продакшна
yarn add express cors
# и для разработки
yarn add -D nodemon
Структура проекта:
- public
- fib.wasm
- index.html
- script.js
- server.mjs
- ...
Обратите внимание на расширение файла server.
Добавляем в package.json команду для запуска сервера для разработки:
"scripts": {
"dev": "nodemon server.mjs"
}
Код сервера (server.mjs):
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'
import express from 'express'
import cors from 'cors'
const __dirname = dirname(fileURLToPath(import.meta.url))
const app = express()
app.use(cors())
app.use(express.static('public'))
app.get('*', (req, res) => {
res.sendFile(resolve(`${__dirname}/${decodeURIComponent(req.url)}`))
})
app.listen(5000, () => {
console.log('
