881894e13d
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>
283 lines
7.7 KiB
Markdown
283 lines
7.7 KiB
Markdown
# 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
|
|
|
|
|
|
|
|
|
|
|