The last miles

John Rose john.r.rose at oracle.com
Tue Aug 22 15:31:21 UTC 2023


On 21 Aug 2023, at 10:39, Dan Smith wrote:

>> On Aug 18, 2023, at 9:15 PM, John Rose <john.r.rose at oracle.com> wrote:
>>
>> I’ve written up in detail how I think Remi’s suggestion can work.
>>
>> https://cr.openjdk.org/~jrose/values/larval-values.html
>>
>> While this is a rough note, I think all the details are present.
>
> The compatibility wins of this strategy do seem nice. But let me scrutinize a few details, because I think there are some trade-offs:
>
> 1) A larval value object is an identity object. This means, in the hand-off between the <init> method and the caller, the object must be heap allocated: the caller and the <init> method need an agreed-upon memory location where state will be set up.
>
> I can see this being optimized away if the <init> method can be inlined. But if not (e.g., the constructor logic is sufficiently complex/large), that's a new cost for value object creation: every 'new' needs a heap allocation. (For <vnew>, we are able to optimize the return value calling convention without needing inlining.)
>
> Am I understanding this correctly? How concerned should we be about the extra allocation cost? (Our working principle to this point has been that optimal compiled code should have zero heap allocations.)

Optimal compiled code can still have this feature, if we choose.

We can direct the compiled version of <init> (for a value class only)
to alter its calling sequence, dropping the input and returning the
value.  Compiled calls to this guy would omit the input.  The
interpreter adapter for it would adjust the discrepancy.  There are
a number of ways to do this, in detail.

(But, also, if the <init> method is complex enough to fail to inline,
we probably won’t notice the extra cost of a buffered input.  If the
<init> method inlines, current JIT optimizations will get rid of the
allocation.  We can, also, adjust inlining heuristics to greatly
favor value-<init>; we do this for certain other kinds of methods
already.  Note that all of these worries only apply to value classes
with non-deprecated constructors.  New code will use factory methods,
which doesn’t need to suffer from failed inlines, again because of
an adjusted heuristic, if we need it.  As I said, there are a number
of ways to address this issue.)

> 2) If we *do* inline the <init> call, then at the call site, there can be any number of references from locals/stack to the larval value, and at the end of the call, there's this unusual operation where all of those locals/stack get transformed into the value object. I *think* this all just falls out cleanly (locals become compiler metadata that bottoms out at the same registers, no matter how many references there are), but it's something to think carefully about.

Let’s continue to think about it, but I have done a first pass and I don’t
see any problem.  (This was surprising to me, so I’m not surprised others
will wish to think about it more as well.)

> 3) The <vnew> approach doesn't have any constraints about leaking 'this', and in particular the javac rule we were envisioning is that the constructor can't leak 'this' until all fields are provably set, but aftewards it's fair game. This <init> strategy is stricter: the verifier disallows leaking 'this' at all from any point in the constructor.

Yes, easy leaking is a feature of <vnew>; the value is always ready.
(This also means the interpreter has to create a new buffer on every
state change.  I don’t care much about interpreter performance, but
I think the <init> version of things performs fewer allocations.)

> Are we okay with these restrictions? In practice, this is most likely to trip up people trying to do instance method calls, plus those who are doing things like keeping track of constructed objects. (Even printf logging seems tricky, since 'toString' is off limits.)

If we wish to allow the super call after all, it can serve as the freeze
point within the constructor.  It is still the case that the freeze must
be performed before the value is usable as an adult, and there is no way
to perform “late” putfields after the freeze.

If the language wishes to fully implement “late” putfields, then we need
to use some new machinery in the translation strategy.  I’d reach for
method/var handles to create withers, in that case.  In other words,
a direct withfield operation would be spun up in a runtime support
API.  We have this code today.

> 4) I'm not sure the prohibition on 'super' calls is actually necessary.

No, but it’s a move of economy.  Defining the meaning of super for
values would be extra work.  We could do that; I’d prefer not to.
Remember that super-constructors for values are already very special
animals:  They must be empty in a special sense.  Forbidding calls to
them seems like the clean move.

> What if, instead, all non-'identity' <init> methods are understood to be working on larval objects, and prohibited from any leaking of 'this'?

Indeed, that is the real issue.  If the user is allowed to leak ‘this’
from <init>, the translation strategy must arrange to promote the leaked
‘this’ to an adult object.  It’s easiest if this is “in place”, which is
how the current verifier rules see it.  But it could also be done with
a method handle built by the runtime, which works like the
finishPrivateBuffer method.  This method handle could take the larval
object and return a fresh adult copy, but without changing the status
of the input larval buffer.

> Instead of disallowing 'super' calls, the verifier would only transition from 'uninitializedThis' to 'LFoo;' in an identity class constructor. Does that make sense or am I missing something?

You are missing the fact that the JVMS allows putfield at that point, to
change the state of an identity object, and this is a bad move for values.
That’s why I think the simple and safe move is to disallow that state
transition; this makes all putfields legitimate, inside the whole <init>
method, without further checks.

> (If it works, does this mean we get support for super fields "for free"?)

That is probably true.  Do we care?

> 5) Do we really need a header state for larval objects? We don't do anything like that to distinguish between uninitialized identity objects (post-'new') and valid identity objects (post-'super()'). We just let the verifier handle it. Same principle here perhaps?

In most cases the header state is not needed.  I mentioned that there are
some potential GC optimizations that (if implemented) would need to see
the larval state and treat it differently.  For compiled code, I think
the larval state would disappear, except for deoptimization logic, which
would “put it back” to larval, for <init> methods that must jump back
into the interpreter.

The relevant JIT optimizations would drop out more or less for free once
the compiled version of <init> had its calling sequence adjusted as noted
above, to drop the input buffer (then it has no state to worry about!)
and just return a value at the end.


More information about the valhalla-spec-experts mailing list