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?
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:
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.
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 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.
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.
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
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.
[Address] = MOV 25 //Single machine instruction to store value
- 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
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.
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