On May 10, 2018, at 3:08 PM, fo...@univ-mlv.fr wrote:
> 
> The strawman strategy is to always consider that you have to send a pointer, 
> so you need to buffer value types before calling a virtual method, if it's 
> not a virtual method you can do the adaptation because you know the caller 
> and the callee. All other strategies should be semantically equivalent.


Calling sequence is just an implementation choice, but there are two
ways it can poke up into the user model.  Here's my understanding of
how that works, and what our options are, FTR.

One of the motivations for using ValueTypes (or ad hoc V/Q variation)
instead of ACC_FLATTENABLE is being able to assign scalarized
calling sequences uniformly across an override tree (i.e. the methods
reached by virtual calls which use the same v-table entry).

Buffering and scalarizing are semantically equivalent except buffering
also supports null.  On the other hand, scalarizing is faster in many cases.

For any given component for the shared method-descriptor of an
override tree, if we can prove that all methods in the tree are null
hostile (throwing NPE on entry and/or never returning null), then
we can hoist this property outside the tree.  We can throw NPE
at the call site, instead of after the virtual dispatch into a method.

Then we get the payoff:  All methods in the override tree may be
compiled to use a scalarized representation of the affected
argument or return type.  All virtual calls into that tree use a
non-buffered representation, too.

There could be an interface default method, or some other method,
which is simultaneously a member of two trees with two different
decisions about scalarization vs. buffering.  This can be handled
by having the JVM create multiple adapters.  I'd rather forbid the
condition that requires multiple adapters as a CLC violation,
because it is potentially complex and buggy, and it's not clear
we need this level of service from the JVM.  If I'm right, this is
one way calling sequences can poke up into the user model.
In the end, the JVM could go the extra mile and spin adapters.

(We spin multiple method adapters, after all, for on-stack
replacement–which is a rare but provably valuable optimization.
First releases of the JVM omitted this optimization, until it was
proven valuable, and then we put in the effort.  I like the move
of deferring optimizations which are not yet proven valuable!)

We can't always get the scalarization payoff, though.  If legacy code
is making virtual calls into the override tree (via the single v-table slot),
*and* if there is at least one legacy method *in* the override tree, then
we can concoct cases where a null is significant and must not be
rejected by the common calling sequence used by the tree.  At
that point buffering is a forced move, or else we declare that the
program does not fully enjoy its legacy behaviors.  (See below
for an example, where 'Bleg' makes a legacy call to its own
legacy

The other way the calling sequence of an override tree pokes
up into the user model is if we declare an override tree to be
hostile to nulls (on some method descriptor component type)
then dynamically loaded legacy code could come late to the
party and add a null-loving method to the override tree.  At
that point, the legacy code cannot fully enjoy legacy semantics.

There's a choice here:  When the legacy method shows up
in a scalarizing override tree, either reject the class on CLC
grounds, or allow the method but firewall it from nulls.
That is, virtual calls to the legacy method will be forbidden from
returning null, and they will never see null arguments, even if the
legacy code is expecting to do something useful with them.
I am proposing the firewall instead of the harsher CLC.

Again, JVM could go the extra mile to make this problem
disappear, by re-organizing the calling sequence of the override
tree as soon as the first legacy method shows up.  For simplicity
I'd rather exclude this tactic until forced by experience to add it.
It seems like a heroic optimization to me, seldom used and likely
to be buggy.  It also seems to spoil the whole performance party
when one bad actor shows up, which looks dubious to me.

I think we need to experiment with a restrictive model that allow
easy scalarization across override trees.  With firewalling of
legacy methods in override trees defined by modern classes,
and with CLC-like rejecting of modern interfaces mixing into
override trees which have already been classified as legacy
trees (w.r.t. some particular method descriptor component).

(FTR, there's also the option of polymorphic calls, where the
the virtual calling sequences does a both-and, passing either
buffered or scalarized arguments, and using some convention
for the caller to say which is which.  The callee would respond
appropriately.  This is likely to be slower than buffering
in some cases, but it could be given an optimistic fast path sort
of like invokeExact has.)

None of these problems occur with distinct Q and L descriptors,
since if you want scalarization you just say Q and legacy code
can't bother you.  L-world adds ambiguity, which is why we are
having this discussion about override trees and calling sequences.

Resolving the ambiguity with ValueType attributes reduces the
complexity of the problem.  In fact, I think the problem is clearly
manageable *if* the JVM is allowed to exclude hard cases on
CLC-like grounds.  If the JVM is required to go the extra mile
and reorganize calling sequences on the fly, then we should
consider whether going back to Q-world is easier, but in that
case the same problems of migration appear elsewhere,
with many adapters, probably more than even the worst
case in L-world.

— John

P.S.  Here's an example of the misadventures of a late-to-the-party
legacy method.

#ValueTypes(Q)
class A { Q m(Q q) { return q; } }
// modern A.m(null) ==> NPE

#ValueTypes(Q)
class A2 extends A { Q m(Q q) { return q; } }
// modern A2.m(null) ==> NPE

A a = p ? new A() : new A2();
// a.m(null) ==> NPE

// (no ValueTypes attr)
class Bleg extends A { Q m(Q q) { return q; } }
// legacy method B.m(null) == null
// choices at this point:
//  - firewall m so it never sees null,
//  - refuse to load Bleg (b/c CLCs)
//  - heroically refactor override tree of A.m

A ab = new Bleg();
// ab.m(null) ==> NPE if firewall
// ab.m(null) == null if heroic refactor (loss of perf. too)

#ValueTypes(A)
class Client {
static {
  Bleg b = new Bleg();
  b.m(null);  //==> NPE b/c of local knowledge
}}

// (no ValueTypes attr)
class Cleg {
static {
  A a = new A2();
  b.m(null);  //==> NPE b/c A2.m is null hostile and/or whole A.m override tree
  Bleg b = new Bleg();
  b.m(null);  //==> NPE b/c if JVM-assigned firewall on Bleg.m <: A.m
  //b.m(null) == null if heroic refactor (loss of perf. too)
}}

Even if Bleg and Cleg privately agree that Q is nullable, if
Cleg makes an invokevirtual of Bleg.m method, it will get the
consensus of the modern override tree of A.m, which is to
throw NPE on null Q.  Bleg and Cleg can be the same class,
in which case the class is making calls to itself, but still gets
null rejection due to a policy decision in a supertype.

Is this tolerable or not?  If not, should we forbid Blog from
loading, on CLC-like grounds?  I'd like to experiment, first
with the firewalling option.

Reply via email to