If you are one of those programmers who on New Year's Eve promised themselves to write faster code, today you have a chance to fulfill this promise. We will talk about how to speed up web solutions using WebAssembly technology (abbreviated as wasm). The technology is very young, now is the time of its formation, however, it may well have a serious impact on the future development for the Internet.
Here I will talk about how to create WebAssembly modules, how to work with them, how to call them from the client code in the browser as if they were modules written in JS. We consider two sets of implementations of
the Fibonacci number search
algorithm . One of them is represented by regular JavaScript functions, the second is written in C and converted to a WebAssembly module. This will allow you to compare the performance of wasm and JS in solving similar problems.
Test Code
We will explore three approaches to finding Fibonacci numbers. The first uses a loop. The second involves recursion. The third is based on the technique of memoization. All of them are implemented in JavaScript and C.
Here is the js code:
')
function fiboJs(num){ var a = 1, b = 0, temp; while (num >= 0){ temp = a; a = a + b; b = temp; num--; } return b; } const fiboJsRec = (num) => { if (num <= 1) return 1; return fiboJsRec(num - 1) + fiboJsRec(num - 2); } const fiboJsMemo = (num, memo) => { memo = memo || {}; if (memo[num]) return memo[num]; if (num <= 1) return 1; return memo[num] = fiboJsMemo(num - 1, memo) + fiboJsMemo(num - 2, memo); } module.exports = {fiboJs, fiboJsRec, fiboJsMemo};
Here is the same thing written in C:
int fibonacci(int n) { int a = 1; int b = 1; while (n-- > 1) { int t = a; a = b; b += t; } return b; } int fibonacciRec(int num) { if (num <= 1) return 1; return fibonacciRec(num - 1) + fibonacciRec(num - 2); } int memo[10000]; int fibonacciMemo(int n) { if (memo[n] != -1) return memo[n]; if (n == 1 || n == 2) { return 1; } else { return memo[n] = fibonacciMemo(n - 1) + fibonacciMemo(n - 2); } }
The subtleties of implementation will not be discussed here, yet, our main goal is different. If you wish, you can read about Fibonacci numbers
here ,
here is an interesting discussion of the recursive approach to finding these numbers, and
here is a material about memoization. Before turning to the practical example, let us briefly dwell on the peculiarities of the technologies related to our conversation.
Technology
WebAssembly technology is an initiative to create a secure, portable, and fast to download and execute code format suitable for the Web. WebAssembly is not a programming language. This is the goal of a compilation that has specifications for text and binary formats. This means that other low-level languages, such as C / C ++, Rust, Swift, and so on, can be compiled into WebAssembly. WebAssembly gives access to the same API as browser JavaScript, organically integrates into the existing technology stack. This distinguishes wasm from something like
Java applets .
The WebAssembly
architecture is the result of community collaboration, in which there are representatives of developers from all leading web browsers. Emscripten is used to compile code into the WebAssembly format.
Emscripten is a compiler from LLVM bytecode in JavaScript. That is, it can be used to compile JavaScript programs written in C / C ++ or in any other languages whose code can be converted to LLVM format. Emscripten provides a set of APIs for porting code to a format suitable for the web. This project has been around for many years, it is mainly used to convert games into their browser versions. Emscripten allows you to achieve high performance due to the fact that it generates code that conforms to Asm.js standards, which is later, but recently, successfully equipped with WebAssembly support.
Asm.js is a low-level, optimized subset of JavaScript that provides linear memory access using typed arrays and supports annotations with information about data types. Asm.js improves the performance of solutions. This is also not a new programming language, so if the browser does not support it, the Asm.js code will be executed as regular JavaScript, that is, it will not be possible to get a performance boost from its use.
WebAssembly, as of January 10, 2017, is supported in
Chrome Canary and
Firefox . In order for the wasm-code to work, you need to activate the corresponding feature in the settings. In
Safari , WebAssembly support is still under development. In V8, wasm is
enabled by default.
Here is an interesting video about the V8 engine, about the current state of support for JavaScript and WebAssembly with the
Chrome Dev Summit 2016 .
Building and loading a module
Let's convert a program written in C to the
wasm format. In order to do this, I decided to take the opportunity to create
standalone WebAssembly modules. With this approach, at the output of the compiler, we get only a file with the code of WebAssembly, without additional auxiliary .js-files.
This approach is based on the Emscripten side module concept. It makes sense to use such modules, since they are, in essence, very similar to dynamic libraries. For example, system libraries do not connect to them automatically, they are some self-contained blocks of code issued by the compiler.
$ emcc fibonacci.c -Os -s WASM=1 -s SIDE_MODULE=1 -o fibonacci.wasm
After receiving the binary file, we only need to load it into the browser. In order to do this,
the WebAssembly API provides a top-level
WebAssembly object that contains the methods needed to
compile and
create an instance of the module.
Here is a simple method based on Alona Zakai's
gist , which works as a universal bootloader.
module.exports = (filename) => { return fetch(filename) .then(response => response.arrayBuffer()) .then(buffer => WebAssembly.compile(buffer)) .then(module => { const imports = { env: { memoryBase: 0, tableBase: 0, memory: new WebAssembly.Memory({ initial: 256 }), table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }) } }; return new WebAssembly.Instance(module, imports); }); }
The best part is that everything happens asynchronously. First we take the contents of the file and convert it into an
ArrayBuffer data structure. The buffer contains the original binary data of a fixed length. We cannot execute them directly, which is why, in the next step, the buffer is passed to the
WebAssembly.compile method, which is returned by
WebAssembly.Module , an instance of which can be created using
WebAssembly.Instance .
Read the description of the
binary format a, which uses WebAssembly, if you want to get a deeper understanding of all this.
Performance testing
It's time to take a look at how to use the wasm module, how to test its performance and compare it with the speed of JavaScript. The input of the functions under study will be given the number 40.
Here is the code for the tests:
const Benchmark = require('benchmark'); const loadModule = require('./loader'); const {fiboJs, fiboJsRec, fiboJsMemo} = require('./fibo.js'); const suite = new Benchmark.Suite; const numToFibo = 40; window.Benchmark = Benchmark; //Benchmark.js uses the global object internally console.info('Benchmark started'); loadModule('fibonacci.wasm').then(instance => { const fiboNative = instance.exports._fibonacci; const fiboNativeRec = instance.exports._fibonacciRec; const fiboNativeMemo = instance.exports._fibonacciMemo; suite .add('Js', () => fiboJs(numToFibo)) .add('Js recursive', () => fiboJsRec(numToFibo)) .add('Js memoization', () => fiboJsMemo(numToFibo)) .add('Native', () => fiboNative(numToFibo)) .add('Native recursive', () => fiboNativeRec(numToFibo)) .add('Native memoization', () => fiboNativeMemo(numToFibo)) .on('cycle', (event) => console.log(String(event.target))) .on('complete', function() { console.log('Fastest: ' + this.filter('fastest').map('name')); console.log('Slowest: ' + this.filter('slowest').map('name')); console.info('Benchmark finished'); }) .run({ 'async': true }); });
And here are the results. On
this page , by the way, you can try everything yourself.
JS loop x 8,605,838 ops/sec ±1.17% (55 runs sampled) JS recursive x 0.65 ops/sec ±1.09% (6 runs sampled) JS memoization x 407,714 ops/sec ±0.95% (59 runs sampled) Native loop x 11,166,298 ops/sec ±1.18% (54 runs sampled) Native recursive x 2.20 ops/sec ±1.58% (10 runs sampled) Native memoization x 30,886,062 ops/sec ±1.64% (56 runs sampled) Fastest: Native memoization Slowest: JS recursive
It is clearly seen that the wasm-code obtained from the C program (in the test output it is designated as “Native”) is faster than the similar code written in regular JavaScript (“JS” in the test output). At the same time, the fastest implementation turned out to be the wasm-function of searching for Fibonacci numbers, using the memoization technique, and the slowest - the recursive function on JavaScript.
If you sit over the results with a calculator, you can find out the following:
- The best-performing C implementation is 375% faster than the best JS implementation.
- The fastest version in C uses memoization. On JS, this is an implementation of an algorithm using a loop.
- The second most efficient implementation on C is still faster than the fastest version on JS.
- The slowest implementation of the algorithm on C is 338% faster than the slowest version on JS.
Results
I hope you enjoyed my short story about the capabilities of WebAssembly, and what can be achieved with this technology today. A lot of topics remain beyond the scope of this material, among which are wasm-modules that are being compiled to create and auxiliary files, various ways of interaction between compiled C code and JS code, dynamic linking. It is possible that we will discuss them someday. Now you have everything you need to start experimenting with WebAssembly. By the way, you can still take a look at the official
guide for WebAssembly
developers .
In order to keep abreast of the latest developments in the area of wasm, add to bookmarks
this page with information about the achievements and development plans of the project. It will be useful to look in the
changelog Emscripten.
By the way, have you already thought about how to use the features of WebAssembly in your projects?