|
| 1 | +# All about Air.js |
| 2 | + |
| 3 | +Air.js is an ES6 benchmark. It tries to faithfully use new features like arrow |
| 4 | +functions, classes, for-of, and Map/Set, among others. Air.js doesn't avoid any |
| 5 | +features out of fear that they might be slow, in the hope that we might learn |
| 6 | +how to make those features fast by looking at how Air.js and other benchmarks |
| 7 | +use them. |
| 8 | + |
| 9 | +This documents the motivation, design, and license of Air.js. |
| 10 | + |
| 11 | +To run Air.js, simply open "[Air.js/test.html](test.html)" in your browser. It |
| 12 | +will only run correctly if your browser supports ES6. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +At the time that Air.js was written, most JavaScript benchmarks used ES5 or |
| 17 | +older versions of the language. ES6 testing mostly relied on microbenchmarks or |
| 18 | +conversions of existing tests to ES6. We try to use larger benchmarks to avoid |
| 19 | +over-optimizing for small pieces of code, and we avoid making changes to |
| 20 | +existing benchmarks because that approach has no limiting principle: if it's OK |
| 21 | +to change a benchmark to use a feature, does that mean we can also change it to |
| 22 | +remove the use of a feature we don't like? We feel that the best way to avoid |
| 23 | +falling into the trap of creating benchmarks that reinforce what some JS engine |
| 24 | +is already good at is to create a new benchmark from first principles. |
| 25 | + |
| 26 | +We only recently completed our new JavaScript compiler, called |
| 27 | +[B3](https://webkit.org/blog/5852/introducing-the-b3-jit-compiler/). B3's |
| 28 | +backend, called |
| 29 | +[Air](https://webkit.org/docs/b3/assembly-intermediate-representation.html), is |
| 30 | +very CPU-intensive and uses a combination of object-oriented and functional |
| 31 | +idioms in C++. Additionally, it relies heavily on high speed maps and sets. It |
| 32 | +goes so far as to use customized map/set implementations - even more so than |
| 33 | +the rest of WebKit. This makes Air a great candidate for ES6 benchmarking. |
| 34 | +Air.js is a faithful ES6 implementation of Air. It pulls no punches: just as |
| 35 | +the original C++ Air was written with expressiveness as a top priority, Air.js |
| 36 | +is liberal in its use of modern ES6 idioms whenever this helps make the code |
| 37 | +more readable. Unlike the original C++ Air, Air.js doesn't exploit a deep |
| 38 | +understanding of compilers to make the code easy to compile. |
| 39 | + |
| 40 | +## Design |
| 41 | + |
| 42 | +Air.js runs one of the more expensive Air phases, Air::allocateStack(). This |
| 43 | +turns abstract stack references into concrete stack references, by selecting |
| 44 | +how to lay out stack slots in the stack frame. This requires liveness analysis |
| 45 | +and an interference graph. |
| 46 | + |
| 47 | +Air.js relies on three major ES6 features more so than most of the others: |
| 48 | + |
| 49 | +- Arrow functions. Like the C++ Air, Air.js uses a functional style of |
| 50 | + iterating most non-trivial data-structures: |
| 51 | + |
| 52 | + inst.forEachArg((arg, role, type, width) => ...) |
| 53 | + |
| 54 | + This is because the functional style allows the callbacks to mutate the data |
| 55 | + being iterated: if the callback returns a non-null value, forEachArg() will |
| 56 | + replace the argument with that value. This would not have been possible with |
| 57 | + for-of. |
| 58 | + |
| 59 | +- For-of. Many Air data structures are amenable to for-of iteration. While the |
| 60 | + innermost loops tend to use functional iteration, pretty much all of the |
| 61 | + outer logic uses for-of heavily. For example: |
| 62 | + |
| 63 | + for (let block of code) // Iterate over the basic blocks |
| 64 | + for (let inst of block) // Iterate over the instructions in a block |
| 65 | + ... |
| 66 | + |
| 67 | +- Map/Set. The liveness analysis and Air::allocateStack() rely on maps and |
| 68 | + sets. For example, we use a liveAtHead map that is keyed by basic block. Its |
| 69 | + values are sets of live stack slots. This is a relatively crude way of doing |
| 70 | + liveness, but it is exactly how the original Air::LivenessAnalysis worked, so |
| 71 | + we view it as being quite faithful to how a sensible programmer might use Map |
| 72 | + and Set. |
| 73 | + |
| 74 | +Air.js also uses some other ES6 features. For example, it uses a Proxy |
| 75 | +in one place, though we doubt that it's on a critical path. Air.js uses classes |
| 76 | +and let/const extensively, as well a symbols. Symbols are used as enumeration |
| 77 | +elements, and so they frequently show up as cases in switch statements. |
| 78 | + |
| 79 | +The workflow of an Air.js run is pretty simple: we do 150 runs of allocateStack |
| 80 | +on four IR payloads. |
| 81 | + |
| 82 | +Each IR payload is a large piece of ES6 code that constructs an Air.js Code |
| 83 | +object, complete with blocks, temporaries, stack slots, and instructions. These |
| 84 | +payloads are generated by running Air::dumpAsJS() phase just prior to the |
| 85 | +native allocateStack phase on the largest hot function in four major JS |
| 86 | +benchmarks according to JavaScriptCore's internal profiling: |
| 87 | + |
| 88 | +- Octane/GBEmu, the executeIteration function. |
| 89 | +- Kraken/imaging-gaussian-blur, the gaussianBlur function. |
| 90 | +- Octane/Typescript, the scanIdentifier function, |
| 91 | +- Air.js, an anonymous closure identified by our profiler as ACLj8C. |
| 92 | + |
| 93 | +These payloads allow Air.js to precisely replay allocateStack on those actual |
| 94 | +functions. |
| 95 | + |
| 96 | +It was an a priori goal of Air.js to spend most of the time in the |
| 97 | +allocateStack phase. This is a faithful reproduction of the C++ allocateStack |
| 98 | +phase, including its use of an abstract liveness analysis. It's abstract in the |
| 99 | +sense that the same liveness algorithm can be reused for temporaries, |
| 100 | +registers, or stack slots. In C++ this meant using templates, while in ES6 it |
| 101 | +means more run-time dynamic dispatch. |
| 102 | + |
| 103 | +Each IR payload is executable code that allocates the IR, and about 15% of |
| 104 | +benchmark execution time is spent in that code. This is significant, but having |
| 105 | +learned this, we don't feel that it would be honest to try to change the |
| 106 | +efficiency of payload initialization. What if the payload initialization was |
| 107 | +more expensive on our engine than others? If it was, then such a change would |
| 108 | +not be fair. |
| 109 | + |
| 110 | +Air.js validates its results. We added a Code hashing capability to both the |
| 111 | +C++ Air and Air.js, and we assert each payload looks identical after |
| 112 | +allocateStack to what it would have looked like after the original C++ |
| 113 | +allocateStack. We also validate that payloads hash properly before |
| 114 | +allcoateStack, to help catch bugs during payload initialization. We have not |
| 115 | +measured how long hashing takes, but it's a O(N) operation, while allocateStack |
| 116 | +is closer to O(N^2). We suspect that barring some engine pathologies, hashing |
| 117 | +should be much faster than allocateStack, and allocateStack should be where the |
| 118 | +bulk of time is spent. |
| 119 | + |
| 120 | +## License |
| 121 | + |
| 122 | +Copyright (C) 2016 Apple Inc. All rights reserved. |
| 123 | + |
| 124 | +Redistribution and use in source and binary forms, with or without |
| 125 | +modification, are permitted provided that the following conditions |
| 126 | +are met: |
| 127 | + |
| 128 | +1. Redistributions of source code must retain the above copyright |
| 129 | + notice, this list of conditions and the following disclaimer. |
| 130 | + |
| 131 | +2. Redistributions in binary form must reproduce the above copyright |
| 132 | + notice, this list of conditions and the following disclaimer in the |
| 133 | + documentation and/or other materials provided with the distribution. |
| 134 | + |
| 135 | +THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| 136 | +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 137 | +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 138 | +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| 139 | +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| 140 | +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| 141 | +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| 142 | +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| 143 | +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 144 | +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 145 | +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 146 | + |
| 147 | +## Summary |
| 148 | + |
| 149 | +At the time that Air.js was written, we weren't happy with the ES6 benchmarks |
| 150 | +that were available to us. Air.js uses some ES6 features in anger, in the hope |
| 151 | +that we can learn about possible optimization strategies by looking at this and |
| 152 | +other benchmarks. |
0 commit comments