« Back to home

Javascript As a Compile Target: A Performance Breakthrough

Things are getting more and more interesting in the font-end community. Whenever we think Javascript has reached its limit, something comes out that pushes it to the next level. Mozilla has been working on a couple of interesting research projects that can redefine the performance of the web. Emscripten is one of them. It is a compiler that compile native languages like C/C++ into highly-performant Javascript code. The format for the compiled Javascript is ASM.JS - recently regarded as the Assembly of the Web.

Why compile to Javascript

The browser can only run Javascript - that's a hard truth that will probably never change. Even though Javascript is a fairly fast dynamic typing language, its performance is still not good enough for things like graphic intensive games. The language's dynamic nature is the main reason for the performance drawbacks, specifically:

  • Type inference: Modern Javascript engines infer type at runtime to make the correct memory for machine instructions. For example, Javascript numbers are all 64-bit floating point, but the Just-in-time compiler may attempt to infer the correct type like 32-bit integer (or more like 31-bit signed integer) to speed up run time memory access. This increases JIT compilation time, resulting in slower application startup.
  • Deoptimization/recompilations: Besides type inference, JS engines also do other optimizations involving type guessing and variable caching. But due to the dynamic nature of the language, variable types may change and caches can be invalidated at any point. When that happens, the engine needs to deoptimize and sometimes even recompile to generate better Assembly.
  • Garbage collection: Garbage collection blocks. The more garbage to collect, the slower it is.

Emscripten is set out to address those drawbacks. It compiles native code into highly optimized Javascript, so that Javascript engines don't need to to just-in-time compilation and optimization. With ASM-supported Javascript engine like OdinMonkey, the optimized Javascript can be compiled ahead-of-time and executed directly. The compiled code can even be cached to minimize subsequent startup time.

How does it work

Compiler front-end like Clang compiles native C/C++ code into LLVM bytecode. Emscripten takes the bytecode and turns it into Javascript instead of machine instructions. The default format for the compiled Javascript is ASM.JS. The main idea behind ASM is that it uses typed arrays as virtual memory. Typed arrays are a set of classes designed for working with raw binary data. There are a few pre-defined arrays like Int8Array, Int16Array, Float32Array, Float64Array…Together they make up the virtual heap for every compiled ASM application. Specifically every generated ASM files contain this piece of code to initialize the virtual memory:

var buffer = new ArrayBuffer(TOTAL_MEMORY);
HEAP8 = new Int8Array(buffer);
HEAP16 = new Int16Array(buffer);
HEAP32 = new Int32Array(buffer);
HEAPU8 = new Uint8Array(buffer);
HEAPU16 = new Uint16Array(buffer);
HEAPU32 = new Uint32Array(buffer);
HEAPF32 = new Float32Array(buffer);
HEAPF64 = new Float64Array(buffer);

Now let's look at what happens when compiling the below piece of C++ code using Emscripten:

int main() {
   printf("hello, world!\n");
  return 1;
}

The generated JS file is 2000 line long. Most of it are internal ASM modules. Here are a couple of interesting parts directly related to the C++ code above:

First is initial memory initialization:

allocate([104,101,108,108,111,44,32,119,111,114,108,100,33,10,0,0], "i8", ALLOC_NONE, Runtime.GLOBAL_BASE);

The method allocate() put an array of data (the character array in this case) of some certain type into the memory heap. The type of this sequence is 8-bit unsigned integer which corresponds to the UInt8Array. ALLOC_NONE tells the method not to allocate on the memory stack just yet. Then in the main function:

function _main() {
  var $1 = 0, $vararg_buffer = 0, $vararg_lifetime_bitcast = 0, label = 0, sp = 0;
  sp = STACKTOP;
  STACKTOP = STACKTOP + 8|0;
  $vararg_buffer = sp;
  $vararg_lifetime_bitcast = $vararg_buffer;
  $1 = 0;
  (_printf(((8)|0),($vararg_buffer|0))|0);
  STACKTOP = sp;return 1;
}

It calls _printf() with a pointer to the beginning of the string and pointer to the argument list residing on the stack. STACKTOP is the pointer to the current location of the stack in the virtual memory. The _printf function formats the output, write the result onto the stack, and then to stdout. After the method execution finishes, stachRestore() is called to restore the stack's top pointer to the default position. This makes sure stack memory only lasts for 1 execution context and will be overriden in subsequent contexts.

function _fprintf(stream, format, varargs) {
  // int fprintf(FILE *restrict stream, const char *restrict format, ...);
  // http://pubs.opengroup.org/onlinepubs/000095399/functions/printf.html
  var result = __formatString(format, varargs);
  var stack = Runtime.stackSave();
  var ret = _fwrite(allocate(result, 'i8', ALLOC_STACK), 1, result.length, stream);
  Runtime.stackRestore(stack);
  return ret;
}

function _printf(format, varargs) {
  // int printf(const char *restrict format, ...);
  // http://pubs.opengroup.org/onlinepubs/000095399/functions/printf.html
  var stdout = HEAP32[((_stdout)>>2)];
  return _fprintf(stdout, format, varargs);
}

This is just a glimpse of what goes on behind the scene of ASM. There are so many more internal modules and libraries included in the generated JS file that's impossible to go through completely. You can find the entire specification for the language here. The spec is not fully implemented yet, but the performance result so far is very promising.

What does the performance look like

According to the benchmarking result by Mozilla, ASM code is about 2x slower than native code running on OdinMonkey, which is comparable to Java and C#. It is promised to get even better up to 70% of native speed after optimizing for float32 operations instead of double64. The result is as follow (lower is better):

img

Current Javascript engines can also run ASM code but still needs to run it through the interpreter and JIT compiler. With such large amount of generated code, the performance in this case is not that good. It's unlikely that Chrome's V8 will optimize for ASM anytime soon. Therefore, Firefox and Mozilla is quite (slightly) ahead in the web performance race.

The future

Game programers will probably benefit the most from Emscripten and ASM. Currently, native games written in a subset of OpenGL can easily be ported to the browser without any additional effort. You can find some demos here.

As for web developers, I don't think the technologies will have a very huge impact. Normal Javascript is already fast enough, and if correctly written, 99% of web applications can run as smoothly as their native counterparts. But who knows, with such powerful tool in their disposal, creative developers can come up with all sort of crazy things. Maybe we'll see a new generation of higly complex interactive websites that are impossible to do with the current web stack.

Other projects

One thing to note here is that Emscripten and ASM are two separated project. ASM is set out to be the universal Javascript compile target, not just for only Emscripten. There are also other compilers like Mandreel or JSIL that can benefit from the format as well. So far, only Emscripten is using ASM as the default compile target, but other projects' implementation is on the way. I'm particularly interested in compiling LLJS to ASM. If ASM is like Assembly, LLJS is like C++ for writing readable and performant low-level code. LLJS already has its own compile target, but with ASM, its performance can get even better.

Comments

comments powered by Disqus