Description
The interceptor calling convention, where the receiver is an explicit parameter, is based on name, which captures methods and selectors that don't really need the convention. Consider add
. JSArray.get$add
and JSArray.add$1
need the convention, but there are user defined classes that define an add
method with two arguments. add$2
does not need interceptor calling convention, but its tear-off getter might, if called from a dynamic call site.
There are currently a few optimizations around the intercepter calling convention
- methods that are not on, or mixed into, a native class ignore the explicit receiver parameter
- when it is determined that the target of an instance method call is one of these methods, a dummy value is passed.
We could make the calling convention decision on a per-callsite basis by putting the 'isIntercepted' bit in the selector. There would be two selectors for add
methods with one argument - add$1U(x)
using the unintercepted convention and add$1I(receiver,x)
using the interceptor convention.
On the classes that currently have an add$1
method that ignores the receiver, the add$1I
method would be a stub that drops the receiver and forwards to add$1U
.
The hope is that most of the these stubs can be removed (i.e. tree-shaken).
We can estimate the size benefit by dropping these ignored receiver parameters and dummy arguments. The results vary widely, the best example being 0.67%, the worst ~0.3%
The cost is harder to estimate. It is difficult to estimate the number of methods that would have both the intercepted and plain calling convention. Worst case is that every method needs a forwarding method to drop the unused receiver. In the 0.67% case above, the size of these stubs wold grow the size by 2.52%, giving net 1.85% larger.
There might be a middle ground that is to implement the per-callsite decision mechanism but only apply it selectively. The current implementation would be equivalent to 'selectively never'.
The program could be analysed on the basis of the static types at call sites to anticipate the number of calls that would benefit from this scheme vs the number of forwarding methods. I think the Map indexer is used often enough to pay for the forwarding stubs on Map implementations (half of the 0.67% is map indexer calls), but I think very few selectors are that obvious a win.
It might be best to do an ad-hoc optimization for indexers. Indexers avoid the complications of tear-offs.
There would be the $index
intercepted indexer and the $get
unintercepted indexer.
In the 0.67% case above, there are 135 indexers that ignore the receiver, i.e. 135 forwarding stubs might be required.
There could be a universal stub on Object that is shadowed by a NoSuchMethod stub on Interceptor. I would not want to do this for List, since I it would penalize all the ListMixin code:
Object.$index: function(receiver, index) { return this.$get(index); }
Interceptor.$index: function(receiver, index) { throw no-such-method }
ListImpl.$index: function(receiver, index) { ... }
MapImpl.$get(index) { ... }
The specializer for indexing would lower the selector to the $get
variant, remove the interceptor argument, and replace itself with another specializer that can still constant-fold but avoids repeated checks.
We would also have to ensure that no native class implements the Map interface.