WebAssembly is “30X” Faster than JavaScript

Or is it? And how not to trust your micro-benchmarks.

Well, let’s do some benchmarking. Here’s a function that counts the number of bits in a 32-bit integer

int waCount(unsigned int x) {
int v = 0;
while(x != 0) {
x &= x - 1;
v++;
}
return v;
}

and of course, we could also write this in JavaScript as

function jsCount(x) {
let v = 0;
while(x != 0) {
x &= x - 1;
v++;
}
return v;
}

And let’s call these functions 100,000 times using a simple loop and see how long they take to execute

let s = 0;
for (let i = 0; i < 100000; i++) {
s += xxCount(i);
}

The results in Chrome are fantastic!!

JS       : 7.26ms
WA : 0.68ms

The WebAssembly version is 10X faster. Can this be? Actually it makes a lot of sense, the C compiler detects that the count function counts the number of bits in a 32-bit integer, and magically translates it to use the i32.popcnt instruction which is then compiled to a single machine instruction on x86. So it makes sense that the WebAssemby code is 10X faster.

Let’s try to see what happens if we manually inline the calls tojsCount. I suspect that there may be some overhead from calling the function in JS.

s = 0;
for (let i = 0; i < 100000; i++) {
let x = i;
while(x != 0) {
x &= x - 1;
s++;
}
}

Sure enough, in Chrome, the JS code is now 6X faster than before and much closer to the WebAssembly version. But this is strange, because we just concluded earlier that the WebAssembly version is translated to a single popcnt instruction, and that the JS code is nowhere nearly as optimal. To have a fair comparison, we should probably inline the C version as well. We can do this by wrapping the benchmark in a C loop (the C compiler will do the inlining for us):

int test(int c) {
int s = 0;
for (int i = 0; i < c; i++) {
s += count(i);
}
return s;
}

Now the JS code is 6X faster than before, but so is the WebAssembly code.

JS Inline: 1.13ms
WA Inline: 0.10ms

In a normal application we probably won’t need to call count 100,000 times in a tight loop, so we’re mostly interested in the performance ofJS Inline vs WA, and from these results we can conclude that WebAssembly doesn’t help that much (even when the generated code is highly optimized). The moral of the story is:

Don’t write tiny WebAssembly functions expecting them to be faster than JS. You’ll most likely be paying for the call overhead and that will outweigh whatever speed benefit you get from WebAssembly in the first place.

Where did the 30X clickbait title come from? The results for JS Inline vs WA Inline in Firefox:

JS       : 2.57ms
JS Inline: 2.49ms
WA : 8.52ms
WA Inline: 0.09ms

Notice that the WebAssembly call overhead is huge here, we should probably fix that.

In summary, think carefully about how you partition your JS / WebAssembly application. You can use WebAssembly code in a piecemeal fashion but you’ll want to execute a moderate amount of code in WebAssembly for it to pay off.

Here’s the fiddle.