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