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

The image below shows the process of executing the program on C on the computer:

Usage example WA

Example code with sources

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 XHRor fetchwhich 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 специальное APIproviding 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 JSmentioned 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 vars 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 using JS across WA
  • wasm-pdf – tool (example) generation PDF-файлов in the browser with JS and WA

Thank you for your attention and have a nice day!


Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *