Species-static members vs singletons (original) (raw)

Brian Goetz brian.goetz at oracle.com
Thu Jun 2 22:24:21 UTC 2016


Brian,**I see that you mentioned that generic instance methods are messier. > The translation for generic instance methods is still somewhat messier (will post separately), but still less messy than if we also had to manage / cache a receiver. Is this issue part of that mess? Do you have a solution, or is this an open issue? I tried making m species statics with a receiver argument, but that makes the invocation non-virtual.

Here’s some more notes on how we might translate generic methods using species-static methods and nested classes.

    General strategy
A {static,species,instance} generic method m() in |Foo| is desugared
into a species method |m()| in a {static,species,instance} nested
class |Foo$m|.
The accessibility of the method |m()| is lifted onto the class |Foo$m|.
Foo also gets an erased bridge, that redirects to the erased
invocation of the generic method (binary compatibility only.)
A generic method is invoked with indy. The indy call site statically
captures the type parameters for the invocation, some representation
of the owning class Foo, and the method m(). The dynamic argument
list captures the Foo-valued receiver (for instance methods) and the
arguments to the generic method.


    Goals
Dispatch should be fast :)
It would be nice if the name-mangling strategy (|Foo$m|) were
private to |Foo| — that it does not appear in bytecode of Foo’s
clients or subclasses.
It would be ideal if we could express what we want with bytecode
alone, not indy, but that does not seem possible in all cases at
this time.


    Static methods

Given source code:

|class Foo { ACC static void m(U u) { } } |

We translate this as:

|@Generic class Foo { // for binary compatibility only @Bridge ACC static void m(Object u) { Foo.Foo$mm(u); } @Generic ACC static class Foo$m { ACC species void m(U u) { } } } |

An invocation |Foo.m(u)| (where U is known statically) can be translated in bytecode as

|invokespecies Foo.Foo$m.m(U) |

However, since it was a goal to not let the mangled name |Foo$m| leak into client code, we can easily wrap this with an indy:

|invokedynamic GenericStaticMethodFoo, "m", descriptor, U ^bootstrap ^static args ^dyn args |

and let the bootstrap put together the class name |Foo$m| from constituent parts (the bootstrap and the compiler share a conspiratorial connection, but the client bytecode doesn’t participate). Since everything is static at the call site, we can link to a |ConstantCallSite| that always dispatches to an |MH[invokespecies Foo$m.m(U)]|.

    Species methods

Species-static generic methods are translated almost the same way; given class

|class Foo { ACC species void m(T t, U u) { } } |

we translate as

|@Generic class Foo { // for binary compatibility only @Bridge ACC species void m(Object t, Object u) { Foo.Foo$mm(t, u); } @Generic ACC species class Foo$m { ACC species void m(T t, U u) { } } } |

and an invocation |Foo.m(T,U)| is translated as

|invokespecies Foo.Foo$m.m(T, U) |

    Instance methods

Our translation strategy — desugaring to a helper class — introduces some challenges in instance method dispatch.

|class Foo { ACC void m(T t, U u) { } } class Bar extends Foo { @Override ACC void m(T t, U u) { } } |

I am proposing we translate this as:

|@Generic class Foo { // for binary compatibility only @Bridge ACC void m(Object t, Object u) { this.Foo$mm(t, u); } @Generic ACC species class Foo$m { ACC species void m(Foo outer, T t, U u) { } } } @Generic class Bar { // for binary compatibility only @Bridge ACC void m(Object t, Object u) { this.Bar$mm(t, u); } @Generic ACC species class Bar$m { ACC species void m(Bar outer, T t, U u) { } } } |

In this proposal, |Bar$m| does not extend |Foo$m|; this is to avoid leaking dependence on desugaring in subclass bytecode. The implementation methods in |Xxx$m| take an extra “outer” argument, which is the receiver for the instance generic method invocation. The use of species-static methods for the implementation methods mean that we need not maintain instances of Foo$m, but instead can pass the actual Fooreceiver directly to the implementation.

For an invocation:

|Foo f = ... f.m(t,u) |

We translate with indy as:

|invokedynamic InstanceStaticMethod[ParamType[Foo,T], "m", descriptor, U](r,t,u) |

The static receiver type — here |Foo| — is a static parameter to the bootstrap. Let’s call this class SR (the actual target will be |Xxx$m| where |Xxx| may be SR or a subclass of SR.) The dynamic receiver (a |Foo|) is passed in the dynamic argument list as |r|.

The linking of the callsite is somewhat complex, but should optimize reasonably well. It proceeds as follows:

The first thing the bootstrap must do (at linkage time) is compute SR. This is done by taking the owner class |Foo|, along with the method name and descriptor, computing the name-mangled class |Foo.Foo$m|, call it SR’. We then take this class and compute |SR=Crass.forSpecialization(SR', U)|. (Both of these computations are done with the classloader for |Foo|, and we should check that both SR’ and SR share the classloader with |Foo|.) SR corresponds to the fully specialized class |Foo.Foo$m|.

Once we’ve computed SR, we have to find the dispatch table for SR, which is a multi-step lookup first by |ClassLoader| (to allow for classloader unloading) and then by SR, which results in a |DispatchTable| mapping a dynamic receiver type |R| to a fully specialized desugared species-static MH.

|class DispatchTable extends ClassValue { ... } class MetaDispatchTable extends ClassValue { ... } private static final WeakHashMap<ClassLoader, MetaDispatchTable> mdt = ... |

We compute |mdt.computeIfAbsent(SR.getClassLoader(), ...).get(SR)| and store that as |DT| in the |CallSite|. The |ClassValue| implementation for |MetaDispatchTable| simply creates a new |ClassValue| entry.

The callsite target is linked to the following logic:

|Class<?> R = r.getClass(); DT.get(R).invokeExact(r, args) |

The |computeValue()| method of |DispatchTable| does the meat of the work. For the receiver type R, we have to find the corresponding |Xxx$m| class, which might be declared in a superclass of R, specialize it to the desired method type parameters, and look up (findSpecies) the appropriate specialized MH. (Finding the corresponding |Xxx$m| class could be done by walking the hierarchy directly, or by doing a |MethodHandle.resolveOrFail| on the erased bridge.)

The cost of an invocation is one |ClassValue| lookup, plus the overhead of folding the arguments together appropriately and doing a MH invoke. The above logic seems representable as a single method handle expression using |fold| and |filter| combinators, but if not, might also introduce some varargs spreading/collecting overhead. (It could be further optimized by wrapping the the result of DT.get(R) with a PIC on R.) This doesn’t seem so bad.

The first time a given target is resolved (a given combination of enclosing |Foo|, |m(...)|, type arguments U, and receiver class), a relatively expensive linkage step is performed — but is then cached in a table specific to the members involved, not the call site — so this should stabilize quickly.

    Interface methods

Interface methods add an additional layer, but do not change the story fundamentally. If I have:

|interface I { ACC void m(T t, U u) { } } class Foo implements I { @Override ACC void m(T t, U u) { } } |

then we need to generate an artifact |I$m| artifact as we do with classes. When linking an invocation which static receiver is a specialization of an interface rather than a class, we compute |I$m| as our SR, and proceed as before. (In this case, our linkage strategy should probably use |resolveOrFail| rather than manual hierarchy walking, so we should probably do this in both cases.) Additionally, we may need to do a check that the |Xxx| class corresponding to the resolved |Xxx$m| holder class actually implements |I| — again this can be done at linkage time, not dispatch time.

    Default methods

If we do our dispatch using |resolveOrFail| against the erased bridges, and the method is not implemented in the receiver’s superclass hierarchy, then I believe that resolution will hand us the MH for the default? If, so, we’re good; we resolve to the default, just like any other implementation.

One integrity risk here is that the |Xxx$m| hierarchy is properly aligned to the |Foo| hierarchy. I think we can validate that (at the time we lazily populate the dispatch table) simply by checking that the resolved erased target |Xxx.m()| and the corresponding specialized class |Xxx$m.m()| share a nest (and hence derive from the same source class.)



More information about the valhalla-spec-observers mailing list