Document describing interceptor optimizations

Change-Id: I02a0413989b5950e21cc6df401ad0f6295614c8d
Reviewed-on: https://dart-review.googlesource.com/62962
Commit-Queue: Stephen Adams <sra@google.com>
Reviewed-by: Sigmund Cherem <sigmund@google.com>
This commit is contained in:
Stephen Adams 2018-07-27 03:29:36 +00:00 committed by commit-bot@chromium.org
parent 55726c8b3e
commit 881894e13d

View file

@ -0,0 +1,282 @@
# Interceptors
## Interceptors
The usual way to add methods to a JavaScript 'class' is to add properties to the
prototype of the constructor function. This works well for user defined
constructor functions. It does not work well for the types provided by the
JavaScript implementation since the prototypes of the builtin types are a shared
resource between all the scripts running in the same page.
**dart2js** maps Dart types to JavaScript builtin types for reasons of
efficiency and compatibility with the browser environment, e.g. a Dart String is
a JavaScript string. In order to avoid unwanted interactions between scripts,
dart2js avoids putting properties on the builtin constructor prototypes.
An interceptor is an object containing the methods and raw type properties of an
instance. An interceptor is a prototype chain without the instance that can be
used in place of the object's actual prototype chain. Having a replacement
prototype chain allows us to make any object, like the builtin numbers and
strings and browser DOM objects, behave like user defined objects. The compiler
can generate interceptor class hierarchies the same as regular class
hierarchies. The difference is that the classes (i.e. JavaScript constructor
functions) are used for their prototypes and never invoked with new.
When the operation `y = x.add(a)` goes via an interceptor the generated
JavaScript code looks like this:
```js
interceptor = getInterceptor(x);
y = interceptor.add$1(x, a);
```
The receiver is passed to the method as an explicit receiver, followed by the
original arguments. The implicit receiver or 'this' argument tracks the type of
the receiver in interceptor space. The call to getInterceptor is a dynamic
dispatch, which in the general case is expensive since it requires a case
analysis of the argument. The code is large too, since every call becomes two
calls in the generated code. Thus is it critical to avoid the full cost
wherever possible.
Interceptors have some properties useful for optimization. The interceptor is a
function of the value, so two calls to getInterceptor with the same argument
will return the same interceptor object. getInterceptor is effect-free.
## Constant interceptors
If the receiver is known to be a leaf class in the inheritance hierarchy, it can
have only one type. In this case, the interceptor is a constant. In this
example, constant FooClass_methods is the same as FooClass.prototype:
```js
interceptor = getInterceptor(x);
y = interceptor.add$1(x, a);
-->
interceptor = FooClass_methods; // constant interceptor
y = interceptor.add$1(x, a);
-->
y = FooClass_methods.add$1(x, a);
```
In addition to replacing the expensive call with a constant, the code now
contains one fewer reference to x. This means that chained code can be
generated with fewer temporaries and tends to be more readable:
```dart
r.x.add(a) // Dart source
```
-->
```js
temp_x = r.get$x();
interceptor = getInterceptor(temp_x);
y = interceptor.add$1(temp_x, a);
-->
temp_x = r.get$x();
interceptor = FooClass_methods;
y = interceptor.add$1(temp_x, a);
-->
y = FooClass_methods.add$1(r.get$x(), a);
```
## Almost-constant interceptors
If the receiver is FooClass or null and all uses of the interceptor for null
should throw noSuchMethod on null, then we can select between the interceptor or
null:
```js
(x && FooClass_methods).add$(r.get$x(), a);
```
Note that we can't do this for num, bool or String since they have falsy values.
(Implemented in SSA).
## Specializations of getInterceptor
getInterceptor is a big if-then-else chain. This can be specialized to the types
possible at the call sites, and specialized to prioritize the types where the
uses of the interceptor succeed. Consider:
```dart
dynamic x = ...
[x.codeUnitAt(0), x.length]
```
The getInterceptor specialization for `x` can be tuned for the use pattern.
It should check for `String` first.
The getInterceptor may omit tests for types that are dominated by a failure (we
don't need to check for `Array` since `x.length` will only be reached for a `String`).
(Part 2 is implemented in SSA).
## Specializations of call sites
### Dummy receiver
If the receiver cannot be an intercepted class and the selector is not used in a
mixin into an interceptor class, then the method does not use the explicit
receiver (the receiver is available as the 'this' parameter). In this case the ignored
explicit receiver can be replaced by any expression.
```dart
findList().add(1)
```
```js
-->
temp = findList$0();
interceptor = getInterceptor(temp);
interceptor.add$1(temp, 1);
-->
temp = findList$0();
temp.add$1(temp, 1);
-->
temp = findList$0();
temp.add$1(0, 1); // dummy receiver
-->
findList$0().add$1(0, 1)
```
It is unlikely that passing 0 as a dummy value is faster, but the code is smaller.
(Implemented in SSA)
## GVN optimizations
`getInterceptor` always returns the same value for the same input and has not
side effects and cannot throw, i.e. the operation is pure.
### CSE
Re-using the result of a dominating call to `getInterceptor` is always beneficial.
### LICM
Hoisting `getInterceptor` calls out of a loop is benefical.
Improvement: `getInterceptor` is expensive, so we generally don't want to hoist
calls out of zero trip loops unless the interceptor is always eventually used,
either in the zero-trip case or somewhere else.
### PRE
Classic partial redundancy elimination (PRE) eliminates computations that are
redundant on some paths.
```dart
a = getList(...);
if (prefix != null) a.add(prefix);
a.add(data);
```
```js
-->
var a, prefix, interceptor, data;
a = getList(...);
if (prefix != null) {
interceptor = getInterceptor(a);
interceptor.add$1(a, prefix);
}
interceptor = getInterceptor(a);
interceptor.add$1(a, data);
-->
var a, prefix, interceptor, data;
a = getList(...);
if (prefix != null) {
interceptor = getInterceptor(a);
interceptor.add$1(a, prefix);
} else {
interceptor = getInterceptor(a);
}
interceptor.add$1(a, data);
```
This is not currently implemented. It would be especially advantageous if the
interceptor is constant on one path.
### Dynamic PRE
```js
var a, interceptor;
while (...) {
a = ...;
if (x != null) {
interceptor = getInterceptor(a);
interceptor.add$1(a, x);
}
if (y != null) {
interceptor = getInterceptor(a);
interceptor.add$1(a, y);
}
}
-->
while (...) {
var a = ...;
if (x != null) {
interceptor = getInterceptor(a);
interceptor.add$1(a, x);
}
if (y != null) {
if (!interceptor) interceptor = getInterceptor(a);
interceptor.add$1(a, y);
}
}
```
## One-shot interceptors
One-shot interceptors are code size optimization that removes the need for a
temporary to hold the receiver.
```js
// r = a.foo() + b;
var t = a.foo$0();
var r = getInterceptor(t).$add(t, b);
-->
$add = function(x, y) {
return getInterceptor(x).$add(x, y);
}
...
r = $add(a.foo(), b);
```
One-shot interceptors are a special case of outlining.
Single-use interceptors used in type tests (`is` expressions) can sometimes be
replaced with `instanceof`-based type tests
https://github.com/dart-lang/sdk/issues/22016 .
### Customized one-shot interceptors
One-shot interceptors for common operations like `+` are customized with a quick
dispatch for common input types.
```js
$add = function(x, y) {
if (typeof x == "number" && typeof y == "number") return x + y;
return getInterceptor(x).$add(x, y);
}
...
// r = a.foo() + b;
r = $add(a.foo(), b);
```
## Sufficing
(Not implemented)
Sufficing is an extension of constant interceptors. If the receiver is known to
be of a non-leaf class in the hierarchy, it might be possible to use the
constant interceptor for the non-leaf class. Explained further:
https://github.com/dart-lang/sdk/issues/22199
## Interprocedural GVN
Shadow field or closed variable
https://github.com/dart-lang/sdk/issues/23686