Let's Write some WASM
Since 2017 there is a new way to run near-native performant code in the browser and that is with a low level language that is compiled from not-so-memory-safe languages like C and C++. Even though the Javascript Engines are doing wonders in optimizing our JavaScript code sometimes you just cannot compete with the raw speed of the low level languages. Lately Web applications needed to do increasingly more work: We have Word and Excel running in browsers and earlier this year Adobe launched Photoshop for the web. Running performant code is paramount for these type of applications and Wasm (WebAssmebly) is here to scratch that itch.
But how much faster is exactly, you might wonder. Well in order to answer that question first we need to write some code, C code. Let's do a recursive implementation of the fibonacci sequence:
#include<emscripten.h>
// this will tell the compiler to expose the function for our javascript runtime
EMSCRIPTEN_KEEPALIVE
long long int fib(long long int n) {
if(n < 1) return 0;
if(n <= 2) return 1;
return fib(n-1) + fib(n-2);
}
int main() { return 0; }Now that we've written our fib implementation we need to compile this code to wasm and for that we're gonna use Emscripten, we run this command in the terminal:
emcc -O2 fib.c -o test.jsthis will output two files, one test.js (which we'll not gonna use for this example) and the second one test.wasm.
Now we're going to create a javascript wrapper to execute our fibonacci implementation on wasm, so let's create a file wasm_fib.js:
const binary = require('fs').readFileSync('test.wasm');
// read the fibonacci value from the arguments passed
const FIB_VAL = Number(process.argv[2]);
if(isNaN(FIB_VAL)) {
console.error('Not a valid number provided');
process.exit(2);
}
WebAssembly.instantiate(binary, {}).then((res) => {
const startTime = Date.now();
const result = res.instance.exports.fib(FIB_VAL);
console.log(result);
const endTime = Date.now() - startTime;
console.log("WASM Duration: ", (endTime/1000).toFixed(2), 'seconds');
});
now we can run in the terminal:
node wasm_fib.js 47
-1323752223
WASM Duration: 4.88 seconds
Our result is a negative number which means that the fibbonaci number was so big that it caused an interger overflow, but we're not gonna worry about that right now because our main interest is the time it took to compute the value: 4.88 seconds. In order to compare this result we also need a javascript implementation, so let's create a new file js_fib.js
const FIB_VAL = Number(process.argv[2]);
if(isNaN(FIB_VAL)) {
console.error('Not a valid number provided');
process.exit(2);
}
const fib = n => {
if(n < 1) return 0;
if(n <= 2) return 1;
return fib(n-1) + fib(n-2);
}
const startTime = Date.now();
const res = fib(FIB_VAL);
console.log(res);
const endTime = Date.now() - startTime;
console.log("JS Duration:", (endTime/1000).toFixed(2), 'seconds');
This is the same implementation but this time we run it directly with javascript:
node js_fib.js 47
2971215073
JS Duration: 9.43 secondsThe javascript implementation didn't cause an integer overflow and it ran in 9.43 seconds so it means that WebAssembly is almost twice as fast as the JavaScript counterpart, let's run another test:
node js_fib.js 50 && node wasm_fib.js 50
12586269025
JS Duration: 43.72 seconds
-298632863
WASM Duration: 20.46 secondsAgain WebAssembly was two times faster. We ran this code in a nodejs environment and I am very curious if there will be any difference in the browser, to test that out we'll need an html page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WASM vs JS Fib battle</title>
</head>
<body>
<button id="button" > Compute </button>
<script>
const FIB_VAL = 50;
let exports;
const fib = n => {
if(n < 1) return 0;
if(n <= 2) return 1;
return fib(n-1) + fib(n-2);
}
WebAssembly.instantiateStreaming(fetch('test.wasm'), {}).then((res) => {
exports = res.instance.exports;
});
document.getElementById('button').addEventListener('click', () => {
const startDate = Date.now();
let val = fib(FIB_VAL);
console.log(val);
const endDate = ((Date.now() - startDate)/1000).toFixed(2);
console.log("JS Duration: ", endDate, 'seconds');
const startWas = Date.now();
const value = exports.fib(FIB_VAL);
console.log(value);
const endWas = ((Date.now() - startWas)/1000).toFixed(2);
console.log("WASM Duration: ", endWas, 'seconds');
});
</script>
</body>
</html>Same as the nodejs example we just load the wasm file and when the Compute button is clicked we execute the fibonacci functions with both Javascript and Wasm implementations. We run our favorite local server to load our index.html file in the browser and we can see the result:
12586269025
JS Duration: 49.78 seconds
-298632863
WASM Duration: 27.59 seconds
[Violation] 'click' handler took 77366msEven if it took a little longer in the browser the WASM implementation remains twice as fast.