a note on WebAssembly

Hello friends!
In 2019 WebAssembly
(Further – WA
or wasm
) has become the fourth “language” of the web. The first three are, of course, HTML
, CSS
and JavaScript
… Today wasm
94% of browsers supported… It is said to provide code execution speed close to native (natural, i.e. the maximum possible for a browser), allowing desktop applications and video games to be ported to the web.
What is wrong with JS
?
JS
Is an interpreted dynamically typed programming language. Dynamic typing means that the type of a variable is checked (determined) at runtime. So what? – you ask. This is how the variable is defined in C++
:
int n = 42
This definition tells the compiler the type of the variable. n
and its location in memory. And all this in one line. And in the case of the definition of a similar variable in JS
(const n = 42
), the engine first has to determine that the variable is a number, then that the number is an integer, and so on. each time the program is executed. Defining and (often) casting (converting) the types of each instruction takes some time.
The process of executing code in JS
looks something like this:
Разбор (парсинг) -> Компиляция и оптимизация -> Повторная (дополнительная) оптимизация или деоптимизация -> Выполнение -> Сборка мусора
And in WA
So:
Расшифровка (декодирование) -> Компиляция и оптимизация -> Выполнение
It does WA
more productive than JS
… In defense JS
it can be said that it was designed to give “light” interactivity to web pages, and not to create high-performance applications that perform complex calculations.
What WA
?
The formal definition is that WA
Is an open bytecode format that allows you to port code written in languages such as C
, C++
, C#
, Rust
and Go
into low-level assembler instructions executed by the browser. Basically, it is a virtual microprocessor that converts a high-level language into machine code.
The image below shows the process of converting a function to add numbers (add
) written in C++
, to binary (binary) format:
note: WA
Is not a programming language. This is a technology (tool) that allows you to convert code in the above languages into machine code that is understandable for browsers.
How WA
works?
WA
Is a web assembler. But what is assembler?
In very simple words, then
- Each processor has a specific architecture, for example,
x86
orARM
… The processor only understands machine code. - Writing machine code, you know, is difficult and tedious. Assembly languages exist to facilitate this process.
- Assembler converts assembly language instructions into machine code that the processor can understand.
The image below shows the process of executing the program on C
on the computer:
Usage example WA
What you need to do to use WA
in the browser (or on the server in Node.js
)? And is it really WA-код
is more productive than JS-код
? Let’s find this out.
Suppose we have such a function on C++
:
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
int ... int
means the function takes an integer and returns an integer. As you can see, our function calculates the sum of the numbers from Fibonacci sequences (hereinafter – fibonaca :)).
First, this function needs to be converted to wasm-модуль
… There are different ways and tools for this. In our case, it is quite suitable for this WasmExplorer
…
Insert the code into the first column, click Compile
to compile the code into Wat
(textual representation of binary format wasm
) and Download
to convert .wat
v .wasm
and download the file (test.wasm
). Let’s rename this file to fib.wasm
…
Let’s prepare a project. We need a server. What for? More on this later.
# создаем директорию и переходим в нее
mkdir wasm-test
cd wasm-test
# инициализируем Node.js-проект
yarn init -yp
# устанавливаем зависимости для продакшна
yarn add express cors
# и для разработки
yarn add -D nodemon
Project structure:
- public
- fib.wasm
- index.html
- script.js
- server.mjs
- ...
note by file extension server
…
Add to package.json
the command to start the development server:
"scripts": {
"dev": "nodemon server.mjs"
}
Server code (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('🚀')
})
Server running at http://localhost:5000
, returns files from directory public
at the request of the client, without CORS
…
Why do we need a server? Because to download (import) wasm-модулей
v JS-код
used by either XHR
or fetch
which will block getting the file from the source file://
(it has to do with security). Running a little ahead, I will say that for import wasm-модулей
exists специальное API
providing several methods. These methods can be conditionally divided into old and new. We will consider both. The bottom line is that when importing a module using the old methods, you can get by with an extension for VSCode
type Live Server
… However, new methods require the header to be present in the response. Content-Type: application/wasm
(seems to be, express
adds such a title automatically based on the file name or its content, but the extension does not).
One more point: we will only consider imports wasm-модуля
v JS
and using the exported from the instance of the module fibonacci. However, we also have the ability to pass functions and variables в wasm-модуль
from JS
…
Let’s look at the markup (index.html
):
<h1>Wasm Test</h1>
<p class="log-c"></p>
<p class="log-js"></p>
<p class="log-comparison"></p>
<script src="https://habr.com/ru/company/timeweb/blog/589793/script.js" type="module"></script>
We have 3 paragraphs for displaying the results of functions, as well as the results of their comparison. We also include the main client script as a module.
Let’s go directly to the client script (script.js
):
const logC = document.querySelector('.log-c')
const logJS = document.querySelector('.log-js')
const logComparison = document.querySelector('.log-comparison')
let fibC
We get links to DOM-элементы
and create a global (within the module) variable for С++-фибоначи
…
async function loadWasmOld(url) {
const response = await fetch(url)
const buffer = await response.arrayBuffer()
const module = await WebAssembly.compile(buffer)
return new WebAssembly.Instance(module)
}
This is the old (conditional) way of loading wasm-модулей
:
- we receive a response (file) from the server
- convert the answer to an array of binary data
- compile the array with
WebAssembly API
- and return an instance of the module
async function initFibC() {
const instance = await loadWasmOld('http://localhost:5000/fib.wasm')
fibC = instance.exports._Z3fibi
}
Variable initialization function fibC
:
- get a copy
wasm-модуля
- assign the exported function to a variable
How do we know the name of the exported function _Z3fibi
? Hence:
function fibJS(n) {
if (n < 2) return n
return fibJS(n - 1) + fibJS(n - 2)
}
JS-фибонача
… By the way, on TypeScript
it will look like this:
function fibJS(n: number): number {
if (n < 2) return n
return fibJS(n - 1) + fibJS(n - 2)
}
Here we explicitly state that the function accepts and returns numbers. But, firstly, this is not necessary, since TS
is able to define it himself (guess of types), secondly, it does not solve problems JS
mentioned above. TS
Is a kind of compromise between static and dynamic (in terms of types) languages.
Let’s execute the fibonac code:
async function run() {
// инициализируем переменную `fibC`
await initFibC()
// выполняем `fibC`
const resultC = fibC(24)
logC.innerHTML = `Результат выполнения функции "fibC" - <b>${resultC}</b>`
// выполняем `fibJS`
const resultJS = fibJS(24)
logJS.innerHTML = `Результат выполнения функции "fibJS" - <b>${resultJS}</b>`
}
run()
Launch the server using the command yarn dev
and go to the address http://localhost:5000
…
Great, the code works. But how do you determine which code is running faster? Easily.
function howLong(fn, ...args) {
const start = performance.now()
fn(...args)
const timeTaken = ~~(performance.now() - start)
return timeTaken
}
This function returns the execution time of the function passed as an argument, in ms (rounded down: ~~
Is an abbreviation for Math.floor
).
Before using this function, let’s rewrite the code to load wasm-модуля
… The new way looks like this:
async function loadWasmNew(url, exportedFn) {
const { module, instance } = await WebAssembly.instantiateStreaming(
fetch(url)
)
return instance.exports[exportedFn]
}
Function loadWasmNew
accepts address wasm-модуля
and the name of the exported function. Method instantiateStreaming
accepts a promise returned by the call fetch
, and returns an object containing a module and an instance WA
… The module can, for example, cache and then use it to create other instances:
const otherInstance = await WebAssembly.instantiate(module)
async function run() {
const fibC = await loadWasmNew('http://localhost:5000/fib.wasm', '_Z3fibi')
const fibCTime = howLong(fibC, 42)
logC.innerHTML = `На выполнение C++-кода потребовалось <b>${fibCTime}</b> мс`
const fibJSTime = howLong(fibJS, 42)
logJS.innerHTML = `На выполнение JS-кода потребовалось <b>${fibJSTime}</b> мс`
}
run()
We see that C++-фибонача
almost 2 times (sic) more productive JS-фибоначи
… Let’s get the exact numbers.
async function run() {
const fibC = await loadWasmNew('http://localhost:5000/fib.wasm', '_Z3fibi')
const fibCTime = howLong(fibC, 42)
logC.innerHTML = `На выполнение C++-кода потребовалось <b>${fibCTime}</b> мс`
const fibJSTime = howLong(fibJS, 42)
logJS.innerHTML = `На выполнение JS-кода потребовалось <b>${fibJSTime}</b> мс`
const differenceInMs = fibJSTime - fibCTime
const performancePercent = ~~(100 - (fibCTime / fibJSTime) * 100)
logComparison.innerHTML = `Код на С++ выполнился быстрее кода на JS на <i>${differenceInMs}</i> мс,<br /> что дает прирост в производительности в размере <b>${performancePercent}%</b>`
}
run()
I believe the hypothesis of higher performance WA
compared with JS
confirmed. Does this mean that web developers urgently need to learn one of the languages compiled into WA
, for the purpose of writing wasm-модулей
and their use in scripts? I do not think. At least for now;)
First, the ecosystem JS
contains a huge number of ready-made solutions for all occasions. Every day something new appears, including something more productive. It will take a long time before a more or less serious infrastructure is formed wasm-модулей
for the web. Anyone who organizes a registry of such modules like npm
(or inside it) will be a great fellow :). Learn from Boris Yankov
– the subregister can, for example, be named @wasm
…
Do you think why Deno
does not “take off”? Because there is “ready” Node.js
… Or GraphQL
? Because pinpoint sampling and data updating can be done through REST
… About RPC
(gRPC
) I will not say anything, because I am not familiar with it from the word “absolutely”, but var
s and callbacks in Quick Start
for Node.js
– this is not serious. A small lyrical digression. note: This is just thinking out loud, not an invitation to discussion.
But who knows what will happen tomorrow? The situation could change dramatically when it becomes possible to import wasm-модули
directly like JS-модулям
– import { _Z3fibi as fibC } from './fib.wasm'
… Or when WA
will be able to manipulate DOM
…
Second, working slowly JS-код
you can almost always do better. Using the example of the same fibonacci:
function fibJS(n) {
let a = 1
let b = 1
for (let i = 3; i <= n; i++) {
let c = a + b
a = b
b = c
}
return b
}
There is more code, but:
The result is calculated instantly.
Perhaps this is all that I wanted to share with you in this article.
Main sources:
A couple of tools:
- webm-wasm – a tool for creating videos in the format
WebM
by usingJS
acrossWA
- wasm-pdf – tool (example) generation
PDF-файлов
in the browser withJS
andWA
Thank you for your attention and have a nice day!