mirror of
https://github.com/dart-lang/sdk
synced 2024-09-18 20:11:19 +00:00
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:
parent
55726c8b3e
commit
881894e13d
282
pkg/compiler/doc/interceptors.md
Normal file
282
pkg/compiler/doc/interceptors.md
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue