EMSCRIPTEN: A PERFECTLY CROMULENT COMPILER


This week, I am going to a deep dive on one of the foundational technologies that makes StarBreak® possible. This will be a bit technical but hopefully it will be interesting to the engineers in the crowd.

BACKGROUND

When I started writing web games in 2007, with Rob Shillingsburg at what would become Wild Shadow, our goal was to make games that were as widely accessible as possible. The choice of tools to use was pretty straightforward: if you wanted an action game that would run on 99% of web browsers at the time, Flash was really your only choice. Although the first iPhone (without Flash) had just been released, Thoughts on Flash was still a few years off so Flash’s future still looked bright.

ActionScript vs. HTML5, can you guess which is which?

However, Flash had a bad reputation among developers. This turned out to be only half deserved though. The Flash compiler/interpreter/runtime was and remained for my entire experience with it, terrible. The performance of code and APIs was often arbitrary. It took a lot of trial and error to figure out how to make a game that could do a lot of draws in reasonable amount of time. The garbage collector was a constant source of problems and even when you avoided allocations completely (which you eventually had to do), you found Flash would allocate memory internally in completely innocuous code for unknown reasons.

In contrast with that, Actionscript 3, the language of Flash, which had been released the previous year, was worlds better than the previous version it replaced. It was based off the ill-fated ECMAScript fourth edition draft but felt much more like Java than JavaScript. It had a well-thought out set of APIs for graphics, event handling, and socket connections (the main ingredients of a multiplayer game) as well as compile-time type checking, classes, packages, namespaces, etc. So even though the code sometimes had to do some strange things to achieve adequate performance, it was still possible to write cool stuff and structure and maintain the code pretty well regardless.

Jumping forward, after I had recovered from years of developing and running RotMG, I got inspired to try making a MetroidVania MMO, an idea that would eventually become StarBreak. It was pretty clear at this point that Flash would eventually be replaced by HTML5 so I bought a bunch of books on JavaScript and began working on a prototype.

JavaScript prototype

Once the prototype was finished it was a great proof of concept but a terrible piece of software. The situation was the reverse of Flash: the underlying interpreter was fast since competition had pushed browser makers to do amazing things, but the language itself made the software unmaintainable. JavaScript just wasn’t designed for large complex software projects like StarBreak. I knew I needed better tools so I investigated lots of solutions: TypeScript, GWT, Haxe, Dart, NaCl, etc. They all had various drawbacks. Luckily, I ended up attending a talk at GDC about a tool that was like an answer to my prayers.

EMSCRIPTEN

Emscripten is an LLVM-to-JavaScript compiler. LLVM is an assembly-like language that is used as an intermediate representation by compilers. The idea is that a high-level language like C++ gets compiled down to LLVM and that is then converted to the machine code to make the executable.
Emscripten instead takes this LLVM code and converts into a subset of JavaScript called asm.js that can run anywhere that JavaScript can run (usually the browser, but also Node.js, etc.) and is easy for browsers to optimize. This means that you can basically write web applications in any language that has an LLVM compiler. For StarBreak, I chose C++ but if you want to write your next web game in Haskell or Fortran, Emscripten makes that possible.

The advantages of this approach are huge:

  • Blazingly Fast: This will seem unintuitive at first but JavaScript created by Emscripten is significantly faster than if you had written it by hand. There are a number of reasons for this.

    • Type information: Variables in JavaScript are untyped but processors need types. This means that JavaScript engines are constantly having to infer types in code to try to make fast native code versions of the JavaScript code. Sometimes this process fails in various ways the impact on the performance is huge. With asm.js, type information is included and variables are never used in a way that would confuse the type inference systems.

    • Compiler optimizations: Because JavaScript is interpreted, there is no time for lengthy analysis to find optimizations. Users won’t wait while your engine tries to find clever ways to skip to a few instructions. However, because the C++ to JavaScript compilation is done offline, there is plenty of opportunity for the compiler to find these optimizations.

    • Memory cache misses: While processors have been getting faster, memory has not kept pace and cache misses are getting more and more important in understanding the performance characteristics of code. Consider the common way of creating an array of points in C++ and JavaScript:

      C++:

      struct Point {int x, int y};
      Point points[1024];
      for (int i = 0; i < 1024; i++) { p[i].x = i; p[i].y = i; }

      JavaScript:

      var points = []
      points.length = 1024;
      for (var i = 0; i < 1024; i++) { points[i] = {x: i, y: i}; }

      Doing any sort of operation on the C++ array will be far more efficient than the same operation on JS array because in the C++ array, all the points are sequential in memory resulting in far fewer cache misses. In a quick test, I did a simple operation on each point in each array. The C++ converted to JS by Emscripten runs at 5x the speed of the handwritten JS code.

  • Right tool for the right job: Programming languages are tools and like all tools, some are better suited for certain tasks than others. As mentioned in the previous dev log about writing AI, Lua has a number of properties that make it more suitable for writing enemy behaviors than C++. However, when we need superfast server code, we use C++ and when we need a production script, we use Python. Writing every web application in JavaScript, regardless of size, complexity, purpose, etc. is like expecting to use the same tools for making a birdhouse or a skyscraper.

  • Native development/Porting: Most weeks, the only day I run Emscripten is on Friday, release day. Unless I’m tracking down a web-specific bug, I do all my development in a native client version of StarBreak. I can use all the normal native client tools (debuggers, profilers, etc.) and just know that when release day comes, all the features and fixes will work on the web just as they did in my native client.

    This also means that should we want to release native client versions of StarBreak, actually getting the game run on most platforms will be relatively straightforward (there are some other issues to sort out with this first though).

  • Code sharing: Because both the StarBreak server and client are written in C++, I don’t have to write code that is shared by both of them twice as I did when working in ActionScript 3 and C++. This saves me a lot of time and avoids a lot of bugs where one piece of code written in one language might act differently than in another language in a subtle and dangerous way.

There are also a few gotchas to keep in mind though:

  • Sockets vs. WebSockets: The browser doesn’t allow access to raw sockets (even though Flash does), so there is a bit of handshake and message header stuff you have to implement (probably via a library) to support WebSockets on the server-side that isn’t necessary with a native or Flash client. Emscripten does a pretty good job papering over the differences in the client but it’s not perfect and you’ll probably end up with the ifdefs in your network code. The WebSocket message header also adds a not insignificant amount of overhead to messages so you generally want to avoid sending lots of very small messages and instead send fewer large messages.

  • Threads vs. Web Workers: The browser doesn’t allow for traditional threads but instead has Web Workers. Web Workers are different enough that attempting to paper over differences as with Sockets/WebSockets just isn’t really possible. In particular, they do not share memory with the main process. So multithreading has to be carefully designed if you want to use it.

  • C++ Libraries: We use Protocol Buffers to serialize data in StarBreak so I needed to compile that library into Emscripten-compatible LLVM. There are some pretty good tools for this but strangely Protocol Buffers has threading code in it for reasons I can not fully explain. This took a bit of work to solve. Other libraries will probably have similar issues.

Emscripten allowed for StarBreak as you see it today:

StarBreak today

THE FUTURE

Although this all works better than I would have predicted, it is without a doubt a bit of a kludge. JavaScript was not intended as a compiler target and even asm.js is not well suited for the purpose. Luckily, the powers that be (i.e. the browser makers) have joined forces to work on WebAssembly. Obviously, this is still in early stages so it isn’t a near or even medium-term solution for anything, but it’s good to know our grandchildren will have a more reasonable way to create web applications for the super-intelligent robots that will enslave us.
-- Wayfarer