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.