Skip to main content
May 7, 2015
Mobile

Bringing Asm.js to Chakra and Microsoft Edge



A couple of months back, we announced that we had started development on Asm.js. Support for Asm.js has been one of the top 10 most-requested items at the Microsoft Edge Developer Suggestion Box on UserVoice since we launched it in December 2014. Since that time, we’ve made good progress, and in Windows 10 Insider Preview builds starting with 10074, we are now previewing the early work that we’ve been doing to enable Asm.js support in Chakra and Microsoft Edge.

What is Asm.js?

Asm.js is a strict subset of JavaScript that can be used as a low-level, efficient target language for compilers. As a sublanguage, asm.js effectively describes a sandboxed virtual machine for memory-unsafe languages like C or C++. A combination of static and dynamic validation allows JavaScript engines to employ techniques like type specialized compilation without bailouts and ahead-of-time (AOT) compilation for valid asm.js code. Such compilation techniques help JavaScript execute at “predictable” and “near-native” performance, both of which are non-trivial in the world of compiler optimizations for dynamic languages like JavaScript.

Given the complexity of writing asm.js code by hand, asm.js is currently produced principally by transpiling C/C++ code using toolchains like Emscripten to run on the Web platform, utilizing technologies such as WebGL and Web Audio. Game engines like those from Unity and Unreal are starting to land early/experimental support of plug-in free games on the web using a combination of asm.js and other related technologies.

How do I experiment with Asm.js in Microsoft Edge?

To enable and experiment with the current Asm.js support in Microsoft Edge, navigate to about:flags in Microsoft Edge and select the “Enable asm.js” flag as below:

Screenshot of about:flags interface for asm.js

Engineering Asm.js into Chakra’s Execution Pipeline

To enable Asm.js in Chakra, the components marked in green below have been added or modified vis-à-vis the typical code flow though Chakra’s pipeline outlined and explained in this blog.

Diagram of the Chakra pipeline

Key changes include:

  • Addition of an asm.js validator, which helps Chakra identify and validate that the asm.js code adheres to the asm.js specification.
  • Generation of optimized Type Specialized bytecode. Given that asm.js supports only native types (int, double, float or SIMD values), Chakra takes advantage of the type information to generate optimized Type Specialized bytecode for asm.js code.
  • Faster execution of Type Specialized bytecode by Chakra’s interpreter by utilizing the type information to avoid boxing and unboxing of numeric values, and removing the overhead of generating profile data while interpreting. For non-asm.js JavaScript code, Chakra’s interpreter generates and feeds profile data to the JIT compiler to enable generating highly optimized JIT code. The type specialized bytecode generated for valid asm.js code already has all the data needed by Chakra’s JIT compiler to produce highly optimized machine code. The type specialized interpreter is 4x-5x magnitude faster than the profiling interpreter.
  • Faster code compilation by Chakra’s JIT compiler. Asm.js code is typically generated by using the LLVM compiler and Emscripten toolchain. Chakra’s JIT compiler takes advantage of some of the optimizations that are already available in the asm.js source code generated by the LLVM compiler. For example, Chakra’s JIT compiler avoids doing an inlining pass and takes advantage of code inlining that has been performed on the asm.js code produced by the LLVM compiler. Avoiding analysis for such JIT optimizations not only helps save CPU cycles and battery usage to generate highly optimized code, but also improves the throughput of Chakra’s JIT compiler tremendously.
  • Predictable performance by removal of bailouts from compiled asm.js code. Given the dynamic nature of JavaScript, all modern JavaScript engines support some form of migration to non-optimized code execution, called bailouts, when they determine that the assumptions made by the JIT compilers are no longer valid for the compiled code. Bailouts not only impact performance but also make it unpredictable. Taking advantage of type-safety restrictions posed on valid asm.js code, the code generated by Chakra’s JIT compiler for valid asm.js code is guaranteed to not require bailouts.

JIT Compiler Optimizations for Asm.js

Apart from changes to Chakra’s execution pipeline described above, Chakra’s JIT compiler has also been enhanced to take advantages of the constraints laid out by the asm.js specification. These enhancements help Chakra’s JIT compiler to generate code that could execute at near native code performance – the golden target for all dynamic language JIT compilers. Let’s drill into two such optimization techniques.

Helper and bailout call elimination

Helper or bailout code acts like a power backup for compiled code in dynamic languages. If any of the assumptions that the JIT compiler makes while compiling the code is invalidated, there needs to be fail safe mechanism to continue execution with correctness. Helpers can be used to handle un-anticipated situations for an operation, and control can then return to the JIT’d code. Bailouts handle situations where the JIT’d code cannot recover from the un-anticipated conditions and control needs to be transferred back to the interpreter to execute the remainder of the current function. For example, if a variable is seen as integer and the compiled code is generated with that assumption, the occurrence of a double should not impact correctness. In such cases, helper or bailout code is used to continue execution, albeit with slower performance. For asm.js code, Chakra does not generate any helper or bailout code.

To understand why, let’s take an example of a function square in an asm.js module, which has been over simplified to help explain the concept.

function PhysicsEngine(global, foreign, heap) {
"use asm";
// Function Declaration
function square(x) {
x = +x;
return +(x*x);
}
}
view raw asm-0.js hosted with ❤ by GitHub

This function square in the code sample above translates to two x64 machine instructions in asm.js code, excluding the prolog and the epilog that is generated for that function.

xmm1 = MOVAPS xmm0 //Move native double value to a temporary register
xmm0 = MULSD xmm0, xmm1 //Multiplication

In contrast, the compiled code generated for the square function when asm.js is not enabled generates over 10 machine instructions. This includes:

  • Checking if the parameter is a tagged double, if not jump to a helper which handles conversions from any type to double
  • Extracting the double value from tagged double
  • Multiplying the value
  • Converting the result back to tagged double

Chakra’s JIT compiler is able to generate efficient code for asm.js by taking advantage of the type information of the variable, which cannot change through the lifetime of the program. The Asm.js validator & linker ensure the same. Given that all internal variables in asm.js are native types (int, double, float or SIMD values), internal functions freely pass native types without boxing them to JavaScript variables between function boundaries. In compiler terminology these are often called direct calls and avoid boxing or conversions when operating on data in asm.js code. Likewise for external calls into or from JavaScript, all the incoming variables are converted to native types and boxed while all the outgoing native types are converted to variables.

Bounds check elimination for typed array accesses

In a previous blog, we explained how Chakra’s JIT compiler supports auto-typed array optimizations, as well as hoisting of bounds check outside of loops thus improving the performance on array operations inside loops by up to 40%. With asm.js type constraints, Chakra’s JIT compiler completely eliminates bounds checking for typed array accesses for all of asm.js compiled code, regardless of the location (inside or outside loops or functions) or the type of the typed array. It also eliminates bounds checks for constant and non-constant index accesses for typed arrays. Together, these optimizations help bring near native performance when operating on typed arrays in asm.js code.

Following is a simplified example of typed array store at a constant offset and the single x64 machine instruction generated by the Chakra’s compiler for that code in asm.js.

int8ArrayView[4] = 25; //JavaScript code
[Address] = MOV 25 //Single machine instruction to store value

When compiling asm.js code, Chakra’s JIT compiler is able to pre compute the final address corresponding to int8ArrayView[4] during link time of asm.js code (first instantiation of the asm.js module) and hence can generate a single instruction corresponding the typed array store. In contrast, the compiled code generated for the same operation in non-asm.js JavaScript code results in around 10-15 machine instructions with path length being considerable. The steps include:

  • Loading the typed array from closure variable (typed array is closure captured in asm.js)
  • Checking if the closure loaded variable is indeed a typed array, and if not, bailout
  • Checking the index is within the length of the typed array; if any of the conditions fail, jumping to helper or bailout
  • Storing the value in the array buffer

The above checks are removed from asm.js code due to the strict constrains on what can be changed inside an asm.js module. For example, a variable pointing to typed array in an asm.js module is immutable and cannot change. If it does, it’s not a valid asm.js code and the asm.js validator fails to validate it as asm.js code. In such cases the code is treated like any other JavaScript function.

Scenario light-ups and performance wins from Asm.js

From the early work that shipped in the latest Windows 10 preview, we have started to see some great scenarios light-up apart from the performance wins by bringing asm.js to Chakra and Microsoft Edge. If you want to take it for a spin, try out games like Angry Bots, Survival Shooter, Tappy Chicken, or try out some fun asm.js demos listed here.

Unity WebGL Player | Prime World Defenders

 In terms of performance wins, there are a crop of asm.js benchmarks that are emerging. With the preliminary asm.js support, Chakra and Microsoft Edge now perform more than 300% faster on Unity Benchmark and around 200% faster on individual tests like zlib, which features in Google’s Octane and Apple’s Jet Stream benchmarking suites.

Unity Benchmark scores for 64-bit browsers
Unity Benchmark scores for 64-bit browsers (Click to enlarge)
(System info: Intel(R) Core(TM) i7 CPU 860 @ 2.80GHz (4 cores), 12GB RAM running 64 bit Windows 10 Preview)
(Scores have been scaled so that IE11 = 1.0, to fit onto a single chart)

 

So, what’s next?

This is an exciting step towards bringing asm.js to the web platform underneath Microsoft Edge. However we are not fully done yet. We are working on fine tuning Chakra’s pipeline for ASM.js support – gathering data to validate if the current design approach performs well for real world asm.js payloads, understanding and addressing outstanding performance, functionality and tooling issues etc. before enabling this feature by default. Once enabled, asm.js would not only be available in Microsoft Edge, but would also be available in Windows store apps written using HTML/CSS/JS as well as Web Views that target the new EdgeHTML rendering engine that is shipping with Microsoft Edge in Windows 10.

We’d also like to thank Mozilla, with whom we’ve been working closely since we started implementing asm.js, and Unity for their support and partnership in helping bring asm.js to Chakra and Microsoft Edge. And a big thanks to all the developers and users of Windows 10 and Microsoft Edge, who have been providing us with valuable feedback in this journey. Keep it coming! You could reach us and let us know your feedback on Twitter at @MSEdgeDev or on Connect.

  • Abhijith Chatra, Senior Software Engineering Manager, Chakra
  • Gaurav Seth, Principal PM Manager, Chakra