Taking a look at Web Assembly
Posted by in Programming onWeb Assembly — WASM for short — is a relatively recent addition to the world of JavaScript development. In many important ways, Web Assembly can be thought of as a sort of hybrid between the now-defunct Java Applets and what I would describe as an Anti-JavaScript.
Where JavaScript is deployed as source code and JIT compiled on the client machine, Web Assembly is distributed as compact pre-compiled binaries. Where JavaScript directly manipulates the browser state through a collection of standardized- and vendor-specific APIs, Web Assembly is containerized and accessed using JavaScript as a proxy. But perhaps the most intriguing and important distinction is that Web Assembly has a significant performance edge over JavaScript.
Every time a JavaScript scope is loaded, it must be locally compiled and executed on the client. This is a great feature for several reasons — not the least of which is that making trivial changes to enormous systems doesn't incur a lengthy build process. With typical web-applications, this isn't a hindrance, since most web applications aren't using hundreds of megabytes of memory. But what if you needed to?
What if your application has really intense calculations or a heavy memory footprint?
If you're using JavaScript, you're kind of out-of-luck. Your code will only run as fast as the JIT compiler will let it and you forego a lot of the benefits of "true" compiled languages. The big one is "optimization".
Sure, some JavaScript engines (such as Google's V8 engine) are able to iteratively improve the performance of commonly hit areas of code. Unfortunately, it's also pretty easy to unnecessarily balloon the memory footprint of a JavaScript application and make it nigh impossible for JavaScript to optimize a particular route of code. For example, using a try-catch or passing a number into a function that otherwise exclusively accepts numbers were once (and may still be) easy ways to prevent Chrome from optimizing a function. Clearly, that's not good enough for high-performance applications.
Enter Web Assembly
I said earlier that Web Assembly is a pre-compiled Anti-JavaScript, but I may have under-stated this a little bit. At the time of this writing, there are no mainstream compilers to convert JavaScript into Web Assembly. If you're going to use Web Assembly, you're going to need to break out your sweet, sweet C, C++, or Rust skills.
C, C++, and Rust have a completely different way of interacting with computers than JavaScript. Say goodbye to runtime type-coercion and "functions are first-class objects". Say hello to the manual memory management!
But Steve! Why would anyone do that to themselves? You (Probably)
For those sweet, sweet RPMs! Unlike JavaScript, Web Assembly runs at about 80% of the speed of a natively compiled application. That's really fast, in case you didn't know.
Let's get to it!
Now that the academia is out of the way, let's get busy building a Web Assembly application!
Build our toolkit
The first step to getting Web Assembly working is to put together your toolkit.
- Install Git on your computer.
- Install CMake on your computer.
- Install a Compiler. I will be using C, and I'm a fan of GCC.
- Install Python 2.7 (not Python 3.x).
Once all of these components are installed, we'll need to clone the source code for the Emscripten compiler (Instructions from MDN):
git clone https://github.com/juj/emsdk.git
cd emsdk
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
Activate the Emscripten environment variables:
# on Linux or Mac OS X
source ./emsdk_env.sh
# on Windows
emsdk_env.bat
Write our Example Code
If we are using Web Assembly for its speed benefits (in this article, I am), then it makes sense to test the speed benefits! The simplest way to check our speed benefits is to see how identical functions perform in JavaScript and Web Assembly. The worse the Big-O of our function, the better of an idea we have of how it'll perform. My personal favorite function for these purposes is the notorious Fibonacci Sequence Generator.
The JavaScript Version
function fib(n) {
if (!n || n <= 0) {
return 0;
}
if (n <= 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
The C Version, stored in test.c
:
#include <emscripten/emscripten.h>
// EMSCRIPTEN_KEEPALIVE exports our function to Web Assembly
int EMSCRIPTEN_KEEPALIVE fib(int n) {
if (n <= 0) {
return 0;
}
if (n <= 1) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
int main(int argc, char** argv) {
return 0;
}
Now, we just need to compile our C file and deploy our code:
# Create a highly optimized (O3) WASM file (WASM=1)
emcc test.c \
-O3 \
-s WASM=1 \
-s NO_EXIT_RUNTIME=1 \
-s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-o test.html
And now you have a Web Assembly file!
I'm not going to lie to you, it's a fairly complicated process. More complicated than I would hope it would be. And technically, we aren't done yet. To use our fib
code, we'll need to import it from the module. I'll spare you the glue code since emcc
generates it for us. Right now, all we need is:
// Create a function that accepts a number parameter and returns a number.
var cfib = Module.cwrap('fib', 'number', ['number']);
And just like that, we have a Fibonacci Sequence Generator function in Web Assembly! But how does it perform? Let's take a look!
Testing the performance:
I chose to do my testing in Node.js v8.9.3, and using the following function to evaluate speeds:
// A simple timer script
const { performance } = require('perf_hooks');
function time(fn, ...args) {
const start = performance.now();
fn(...args);
return performance.now() - start;
}
It's a simple timer script to get some quick insight into how long it takes to execute a function. The results were somewhat surprising:
Getting the 40th Fib Number
> time(fib, 40)
1563.8281759917736 // (1.6 seconds in JavaScript)
> time(cfib, 40)
813.6897919774055 // (0.8 seconds in WASM)
Getting the 45th Fib Number
> time(fib, 45)
17652.82110801339 // (17.7 seconds in JavaScript)
> time(cfib, 45)
8975.838954001665 // (9.0 seconds in WASM)
Getting the 50th Fib Number
> time(fib, 50)
255291.12804800272 // (255.3 seconds in JavaScript)
> time(cfib, 50)
107295.33666801453 // (107.3 seconds in WASM)
From my testing, it looks like the performance on this absolutely horrendous abomination of a resource-hogging function is actually about twice as fast in Web Assembly than it is in JavaScript. And there is the niche.
To sum it up
Web Assembly has a lot of limitations. It's difficult to configure, and it requires you to have a working knowledge of some other low-level programming language. With that said, if you need to do some really heavy lifting, and JavaScript isn't up to snuff, and you really need to make sure it works on any architecture, then maybe Web Assembly is the right solution for you.
Enjoy!