By Philippe Leefsma (@F3lipek)
I meant to take a look at the topic since a while but finally got time during a rainy weekend: asm.js, "an extraordinarily optimizable, low-level subset of JavaScript" and Web Assembly, "an experimental efficient low-level programming language for in-browser client-side scripting" !
You can easily find lots of articles on that topic over the web, so I'm not planning to explain in details the theory... Rather than summing up again the same information from different sources, this blogpost is a deep dive from my own experience into digging how to compile a non-trivial piece of C++ into asm.js and Web Assembly (wasm). Then running them inside the browser from a custom test in order to benchmark the performances. Results are satisfying, to say the least.
If you are ready for a sneak peek at the future of web development, keep reading!
I - Setting up the Emscripten tool chain
The first step in the process is to setup the required tools that enable you to compile a piece of C++ into something that can be loaded into a browser. That project is called Emscripten, so the first thing you would do when opening the link, is to go to the Getting Started section and follow the instructions right?
Yes, but there is a trick... the standard install instructions are suitable to compile C++ into asm.js, but if you plan to compile into Web Assembly - I know you do - there is just a little difference: instead of installing the latest SDK, you need to install the incoming branch, which has BINARYEN compile option available out of the box.
1/ Go to Emscripten SDK download page and make sure you have the prerequisite tools on your machine. The SDK runs on OSX, Windows and Linux
2/ You will then build the SDK from source as per those instructions. You just need to instal the sdk-incoming-64bit as described in the tutorial, you do NOT need to install the master branch (no wasm support there yet - at the time of this writing)
3/ Perform a check to see if emcc (the Emscripten compiler) is installed properly, I have it in my system path, so I can simply type emcc -v in a terminal:
If you see something like this, then you are ready for the next thrill!
II - Back to the 90's, let's write some C++
It's been a while since I hadn't written more than few lines of C++. Emscripten documentation is pretty good and the SDK comes which a bunch of samples, ranging from very basic to pretty advanced, so you probably want to go at least through the basic hello world tutorial.
It's quite detailed, so I assume this goes smoothly on your side as well and you are now able to compile a single C++ file into asm.js, then load it into the demo web page that is produced when using the -o output.html:
./emcc tests/hello_world.c -o hello.html
Almost the same emotion than the first C++ program I wrote on my PC!
My goal here however is to write a slightly more advanced piece of code in order to compare its execution speed with plain JavaScript or we could say "human written JavaScript". A good candidate for that task that came naturally to my mind is the particle system I wrote few months ago, I already have the JavaScript ES6 version, so I just needed to write an equivalent C++ version.
Below is how the console test of my C++ particle system looks like. Note: it is just used to test and debug the particle system while writing it, this is not the actual test this blog is about.
I wrote and compiled the project initially in Visual Studio 2010 (for compatibility reason with the tools), then used a tool called make-it-so that will parse the Visual Studio .sln solution and generate a make file out of it.
III - Exposing C++ classes to JavaScript
Alright, at this step we have a C++ particle system that compiles and can run in console mode as a pure C++ application. We can compile it using the make file produced by MakeItSo - or created by hand if you fancy this kind of thing. However what we want is to be able to instantiate the ParticleSystem from our JavaScript code, so we can really leverage the speed gain from a web application mostly written in js.
In order to achieve that, Emscripten offers two different approaches:
1/ Embind: basically you declare the bindings between the C++ class methods and properties and the generated JavaScript objects using the EMSCRIPTEN_BINDINGS macro
2/ WebIDL Binder: you create an extra .idl file that describes the bindings, generate a .js and .cpp files by running a python script from the SDK against the idl and finally compile the project including that cpp file.
The second option may look like like a lot of extra steps, however this is the one I chose. I felt that the main advantage is that you don't have to modify your initial C++ code by adding macro to each file and link the project to the correct emscripten C libraries, so you can keep your C++ code base intact and keep developing and testing as is. Note that this is also the approach chosen by two major Emscripten C++ ports: box2d.js and ammo.js (Bullet Physics)
Here are the bindings definition for my project so far:
Once the bindings are correct, you will generate the glue files by invoking a python script as follow - path will depends on your emsdk location:
python ~/emsdk/emscripten/incoming/tools/webidl_binder.py bindings.idl glue
This produces glue.cpp and glue.js, you can then create an extra file glue_wrapper.cpp which includes the required headers and glue.cpp, you then add glue_wrapper.cpp to your makefile. This is suggested this way so if you modify your bindings in the future, you can simply re-run the python script without editing any other file in your project. Refer to WebIDL section on the official documentation for more details.
If everything is done correctly, you should now be able to replace g++ compiler by emcc in the makefile and by running make, it will produce the byte code: ParticleSystem.bc. You can take a look at my custom makefile there.
IV - Generating asm.js
We are just one step away from generating asm.js: just run the following command by specifying the glue.js as post-js step, the ParticleSystem.bc from the compilation and also NO_EXIT_RUNTIME=1 option to indicate that even after main has returned, we keep the runtime alive because our JavaScript code will be running the particles. I also comment out all the test code inside the C++ main as it is irrelevant.
emcc -s NO_EXIT_RUNTIME=1 -s release/ParticleSystem.bc --post-js ParticleSystem/glue.js -o dist/asmjs/ParticleSystem.asm.js
In my project I created the emcc-asmjs.sh script for not having to type the command every time. The output file is valid ES5 JavaScript file ParticleSystem.asm.js that can simply be included in your html using a classic <script> tag. You can take a look at the asm.js test, which is invoking the exported C++ classes, it's pretty close to the C++ code we had originally in main.cpp.
V - Generating Web Assembly
That's the cherry on the top, by simply adding the BINARYEN=1 option - that's where using the incoming branch of Emscripten matters - you can generate wasm files!
emcc -s NO_EXIT_RUNTIME=1 -s BINARYEN=1 release/ParticleSystem.bc --post-js ParticleSystem/glue.js -o dist/wasm/ParticleSystem.js
In that case it produces several files and loading the script in your html requires a bit more trickery:
1 <script> 2 3 var Module = Module || { 4 wasmBinaryFile: '../Emscripten/dist/wasm/ParticleSystem.wasm' 5 } 6 7 var xhr = new XMLHttpRequest() 8 xhr.open('GET', '../Emscripten/dist/wasm/ParticleSystem.wasm', true) 9 xhr.responseType = 'arraybuffer' 10 11 xhr.onload = function() { 12 13 Module.wasmBinary = xhr.response 14 15 var script = document.createElement('script') 16 script.src = "../Emscripten/dist/wasm/ParticleSystem.js" 17 document.body.appendChild(script) 18 } 19 20 xhr.send(null) 21 </script>
Testing the wasm can be achieved with the exact same code than for asm.js, I only added a check on Module to see if ParticleSystem object is available as the laoding is asynchronous. It might not be the best approach but reliable enough for now...
VI - Benchmarking the code
Finally the fun part is to compare how well asm.js and wasm perform against the plain JavaScript code, well my JavaScript code was written in ES6, so it still has to go through a build step to transpile it.
Asm.js is plain JavaScript so any current browser should be able to load it, wasm is experimental so it has support only in Chrome Canary and Firefox Nightly at this time, with Microsoft Edge coming pretty soon.
I created a simple interface using jsoneditor which allows to modify the test configuration. You can tweak the time step, number of particles, number of steps, add some emitters and fields. There is no validation so if you provide invalid or missing parameters, you will probably crash the test. You can also output the particles position to the browser console with dumpParticles set to true, so don't output 10,000 particles over 10,000 steps, you may sit there for a while...
Loading the wasm on a browser that doesn't support will use a fallback, so the code still runs but ends up being very very slow. You can give it a try but be warned.
That's what you can see in your browser console if it can successfully load the wasm file. In Canary it worked straight ot of the install, in Firefox Nightly I had to activate in the config flags javascript.options.wasm: true
I created two tests, here are the live versions: ES6 vs ASM.js and ES6 vs WASM and here is a plot of the results I gathered across several browsers, they were using the same default config 1000 particles over 5000 steps:
Safari comes the very last as far as ES6 is concerned with a huge difference, however the asm.js executes almost the fastest on it with a 42x speed factor increase!
Firefox Nightly is the one that executes the asm.js code the fastest with an elapsed time of 122 ms. Oddly it doesn't seem to handle wasm very well with a total time of 521 ms, so much higher in comparison
Canary is the absolute winner of the contest with an execution speed of 121 ms for the wasm version, 12x faster than it runs ES6! However it doesn't handle asm.js as well as Firefox or Safari ...
That's it for now! I hope you enjoyed, that was a pretty interesting experiment for me so far. The next step will be integrate my particle system asm.js and wasm versions into a Forge Viewer sample and see how much they can increase the FPS of the simulation, so I will definitely blog some more of that in the future.
The full git project is available at https://github.com/leefsmp/Particle-System with instructions on how to build it.