As we all know, machines read binary codes (strings of zeros and ones) because it is very convenient for them but not for humans. So engineers basically created a mapping from binary code and called the mapped version 'Assembly'.
WebAssembly is an 'Assembly'-like language. It's nickname is wasm. Basically WebAssembly is the Assembly language for modern browsers.
People usually don't write Assembly code directly because it can be tedious. That's why there are many Assembly compilers that simply create Assembly code from C/C++, C#, Rust, Java ... So basically you can run an existing C/C++ or Rust application inside a browser.
WebAssembly code can be directly injected into Javascript or Node.js environment. This can be very useful because you can combine WebAssembly with Javascript.
WebAssembly provides near native performance. Javascript is just-in-time (JIT) compiled. But WebAssembly is pre-compiled. So that's why wasm can be executed faster than Javascript code.
Firstly, all the source codes used in this post available in GitHub. The ultimate question in my mind is "Can we sort an array of integers faster with WebAssembly?". Since we want to execute faster, I think I should use the holy programming language C. To compile WebAssembly, I used emscripten I wrote C code and compiled C code into WebAssembly using emscripten. I simply followed instructions at MDN and emscripten
After you download and install emscripten by following their documentation, you should have a emsdk
folder. Go into that folder and run source ./emsdk_env.sh
. Now you can compile your C codes into WebAssembly. To compile a hello world WebAssembly code, I used command emcc -o hello3.html hello3.c --shell-file html_template/shell_minimal2.html -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']"
. Here emcc
is the emscripten compiler. -o hello3.html hello3.c
means compile 'hello3.c' and output HTML file 'hello3.html'. My 'hello3.c' is like below
#include <stdio.h>
#include <emscripten/emscripten.h>
int main() {
printf("Hello World\n");
return 0;
}
#ifdef __cplusplus
#define EXTERN extern "C"
#else
#define EXTERN
#endif
EXTERN EMSCRIPTEN_KEEPALIVE void myFunction(int argc, char ** argv) {
printf("MyFunction Called\n");
}
It simply prints 'Hello World' to the developer console when the web page is loaded. And also prints 'MyFunction Called' when the C function is called.
--shell-file html_template/shell_minimal2.html
of the script basically uses a template html file to generate the output 'hello3.html' file. My 'shell_minimal2.html' is like below.
<html>
<body>
<button id="mybutton">Call the C function</button>
</body>
</html>
{{{ SCRIPT }}}
<script>
document.getElementById("mybutton").addEventListener("click", () => {
alert("check console");
const result = Module.ccall(
"myFunction", // name of C function
null, // return type
null, // argument types
null // arguments
);
});
</script>
The rest of the command is necessary to call the function using some special method called ccal
. If you execute the command, it will generate 'hello3.html', 'hello3.js', and 'hello3.wasm' files. If you look at look at 'hello3.html' you will see
<html>
<body>
<button id="mybutton">Call the C function</button>
</body>
</html>
<script async type="text/javascript" src="hello3.js"></script>
<script>
document.getElementById("mybutton").addEventListener("click", () => {
alert("check console");
const result = Module.ccall(
"myFunction", // name of C function
null, // return type
null, // argument types
null // arguments
);
});
</script>
The only difference between the template file 'shell_minimal2.html' and 'hello3.html' is the {{{ SCRIPT }}}
section. Inside the 'hello3.html', this part is replaced with <script async type="text/javascript" src="hello3.js">
'hello3.js' is a Javascript file that provides a global variable named Module
and also imports hello3.wasm
. hello3.wasm
is a binary file. You cannot read it with naked eye easily. Start an HTTP server and open 'hello3.html' in a modern browser. I'm using VSCode for code editing and it has a nice extension for simple HTTP server. Then you can actually see that wasm file is converted to WebAssembly Text (.wat) format in your browser.
In developer console, you can see printf
statements of C codes are actually printed. This is hello world for WebAssembly! Also if you click to the button, you will see it calls a C function! That's amazing! From JavaScript we can call a C function!
Now let's see if a C code can execute faster then plain Javascript code. To make a comparison I implemented fibonacci algorithm in a very inefficient way in both C and Javascript and executed side-by-side.
I executed command just like hello world example emcc -o hello4.html fib.c --shell-file html_template/simple_compare.html -sEXPORTED_FUNCTIONS=_fib -sEXPORTED_RUNTIME_METHODS=cwrap
. Here to call a C function, we use someother special method cwrap
.
Below is my C code file 'fib.c'
int fib(int n)
{
if (n < 2)
return n;
return fib(n - 1) + fib(n - 2);
}
Here is my 'simple_compare.html' file
{{{ SCRIPT }}}
<input type="number" id="fibNum" value="35" />
<button id="mybutton">Compare JS vs C on Fibonacci</button>
<script>
document.getElementById("mybutton").addEventListener("click", () => {
const fibIndex = Number(document.getElementById("fibNum").value);
const t1 = executeFibonacciOnC(fibIndex);
const t2 = executeFibonacciOnJS(fibIndex);
console.log("C time:", t1, "JS time:", t2);
});
// finds the 'n'th fibonacci number in C using the worst implementation and returns the execution time
function executeFibonacciOnC(n) {
fibC = Module.cwrap("fib", "number", ["number"]);
const t1 = performance.now();
const res = fibC(n);
const t2 = performance.now();
console.log("c result: ", res);
return t2 - t1;
}
function executeFibonacciOnJS(n) {
const t1 = performance.now();
const res = fib(n);
const t2 = performance.now();
console.log("JS result: ", res);
return t2 - t1;
}
function fib(n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
</script>
Here you can see that Javascript is a lot faster then C code. I feel like I wasted all my time and WebAssembly is just a balloon.
Then I realized there is some compiler optimization flags in emscripten. Let's use them emcc -o hello4.html fib.c --shell-file html_template/simple_compare.html -sEXPORTED_FUNCTIONS=_fib -sEXPORTED_RUNTIME_METHODS=cwrap -O2
I just added a -O2
flag to my command and do it again.
Now you can see some magic! This time, C code executes faster! In some cases it is like even 2 times faster!
Now let's try our ultimate aim. Can we sort numbers faster then Javascript? Let's try with C standard library function qsort
I think it should be faster, C codes are usually fast. Similar to previous command, I executed command emcc -o qsort.html arraySorter.c --shell-file html_template/simple_compare_array.html -sEXPORTED_FUNCTIONS=_arraySorter,_malloc,_free -sEXPORTED_RUNTIME_METHODS=cwrap
My 'arraySorter.c' is very simple like below. It simply calls qsort
function with a comparer function.
#include <stdlib.h>
int compareFn(const void *a, const void *b)
{
return (*(int *)a - *(int *)b);
}
void arraySorter(int *arr, int size)
{
qsort(arr, size, sizeof(int), compareFn);
}
My template file 'simple_compare_array.html' is like below. Here passing and array as a parameter is bit hard. We need to use malloc
and free
functions to create and array and pass it to C.
{{{ SCRIPT }}}
<input type="number" id="arrSize" value="1000000" />
<button id="mybutton">Compare JS vs C on sorting integer array</button>
<script>
function getArray() {
const l = Number(document.getElementById("arrSize").value);
return Array.from({ length: l }, () => Math.floor(Math.random() * l));
}
document.getElementById("mybutton").addEventListener("click", () => {
const arr1 = getArray();
const arr2 = Array.from(arr1);
const t1 = arrayOperationsOnC(arr1);
const t2 = arrayOperationsOnJS(arr2);
console.log("C time:", t1, "JS time:", t2);
});
function arrayOperationsOnC(n) {
const BYTE_SIZE_OF_INT = 4;
const t1 = performance.now();
const arraySize = n.length;
const arrayPointer = Module._malloc(arraySize * BYTE_SIZE_OF_INT);
Module.HEAP32.set(new Int32Array(n), arrayPointer / BYTE_SIZE_OF_INT);
const cFunc = Module.cwrap("arraySorter", null, ["number", "number"]);
cFunc(arrayPointer, arraySize);
const resultArray = Array.from(
Module.HEAP32.subarray(
arrayPointer / BYTE_SIZE_OF_INT,
arrayPointer / BYTE_SIZE_OF_INT + arraySize
)
);
console.log(resultArray);
Module._free(arrayPointer);
const t2 = performance.now();
return t2 - t1;
}
function arrayOperationsOnJS(n) {
const t1 = performance.now();
const res = arraySorter(n);
console.log(res);
const t2 = performance.now();
return t2 - t1;
}
function arraySorter(arr) {
return arr.sort((a, b) => a - b);
}
</script>
If I execute this I see my C code is like 4 times slower! What the heck is wrong?
OK I see I didn't use compiler optimization flags. Let's try -O2
flag and do it again. Now it is faster but still very slower then Javascript.
C time: 716.1 JS time: 269.5
Even if I use -O3
flag, I see it is still slower. There is no '-O4' this is the most optimized.
C time: 711.9 JS time: 270.6
Now we are sure that quick sort with C cannot pass plain Javascript. But we used C standard library function qsort
. Can we try plain C code for quick sort? Let's try. I used command emcc -o faster_sorter.html arraySorter2.c --shell-file html_template/simple_compare_array.html -sEXPORTED_FUNCTIONS=_arraySorter,_malloc,_free -sEXPORTED_RUNTIME_METHODS=cwrap -O2
Here I used the same HTML template but this time I implemented quick sort algorithm with plain C code. Below in my arraySorter2.c
file.
// Quick sort in C
// function to swap elements
void swap(int *a, int *b)
{
int t = *a;
*a = *b;
*b = t;
}
// function to find the partition position
int partition(int array[], int low, int high)
{
// select the rightmost element as pivot
int pivot = array[high];
// pointer for greater element
int i = (low - 1);
// traverse each element of the array
// compare them with the pivot
for (int j = low; j < high; j++)
{
if (array[j] <= pivot)
{
// if element smaller than pivot is found
// swap it with the greater element pointed by i
i++;
// swap element at i with element at j
swap(&array[i], &array[j]);
}
}
// swap the pivot element with the greater element at i
swap(&array[i + 1], &array[high]);
// return the partition point
return (i + 1);
}
void quickSort(int array[], int low, int high)
{
if (low < high)
{
// find the pivot element such that
// elements smaller than pivot are on left of pivot
// elements greater than pivot are on right of pivot
int pi = partition(array, low, high);
// recursive call on the left of pivot
quickSort(array, low, pi - 1);
// recursive call on the right of pivot
quickSort(array, pi + 1, high);
}
}
void arraySorter(int *arr, int size)
{
quickSort(arr, 0, size - 1);
}
Now it seems with -O2
flag we can sort integers faster than plain Javascript! Now C code is like 2 times faster. That's amazing!
C time: 156.8 JS time: 271.5
Is Wasm faster in your browser? Try and see now. All the source codes is available under MIT license.