User model stacking: current status

Dan Heidinga heidinga at redhat.com
Thu May 5 21:03:54 UTC 2022


Thanks for writing this up.  I've been thinking and playing with
related ideas and this gives a clear starting point for us to work
off.

On Thu, May 5, 2022 at 3:21 PM Brian Goetz <brian.goetz at oracle.com> wrote:
> There are lots of other things to discuss here, including a discussion of what does non-atomic B2 really mean, and whether there are additional risks that come from tearing _between the null and the fields_.
>
> So, let's discuss non-atomic B2s.  (First, note that atomicity is only relevant in the heap; on the stack, everything is thread-confined, so there will be no tearing.)
>
> If we have:
>
>     non-atomic __b2 class DateTime {
>         long date;
>         long time;
>     }
>
> then the layout of a B2 (or a B3.ref) is really (long, long, boolean), not just (long, long), because of the null channel.  (We may be able to hide the null channel elsewhere, but that's an optimization.)
>
> If two threads racily write (d1, t1) and (d2, t2) to a shared mutable DateTime, it is possible for an observer to observe (d1, t2) or (d2, t1).  Saying non-atomic says "this is the cost of data races".  But additionally, if we have a race between writing null and (d, t), there is another possible form of tearing.
>
> Let's write this out more explicitly.  Suppose that T1 writes a non-null value (d, t, true), and T2 writes null as (0, 0, false).  Then it would be possible to observe (0, 0, true), which means that we would be conceivably exposing the zero value to the user, even though a B2 class might want to hide its zero.

And to be pedantic, it may also be possible to observe (d, 0, true),
(0, t, true).

> So, suppose instead that we implemented writing a null as simply storing false to the synthetic boolean field.  Then, in the event of a race between reader and writer, we could only see values for date and time that were previously put there by some thread.  This satisfies the OOTA (out of thin air) safety requirements of the JMM.

If we only need to write the null channel to make something null,
there still may be cases where all fields might be written when
writing the null resulting in writing (g1, g2, false) and  unless this
is outlawed by the spec, it may still generate OOTA values.  Where
would we do this?  Possibly in the interpreter to avoid (another)
conditional branch in the putfield logic or as you say next, to null
out OOPs.

> The other consequence we might have from this sort of tearing is if one of the other fields is an OOP.  If the GC is unaware of the significance of the null field (and we'd like for the GC to stay unaware of this), then it is possible to have a null value where one of the oop fields (from a previous write) is non-null, keeping that object reachable even when it is logically not reachable.  (As an interesting connection, the boolean here is "special" in the same way as the synthetic boolean channel is in pattern matching -- it dictates whether the _other_ channels are valid.  Which makes nullable values a good implementation strategy for pattern carriers.)

This is important to call out because it's non-obvious - making the GC
aware of the null channel is insufficient to make this safe.  If the
GC is aware of the null channel and doesn't keep the OOPs alive, it is
possible for another thread to tear the null channel and the OOP
access and read an invalid object pointer.  The GC either needs the
OOP fields to be nulled - exposing the zeros - or to treat them as a
live.

>
> So we have a choice for how we implement writing nulls, with a pick-your-poison consequence:
>
>  - If we do a wide write, and write all the fields to zero, we risk exposing a zero value even when the zero is a bad value;
>  - If we do a narrow write, and only write the null field, we risk pinning other OOPs in memory
>

I'm sure we all remember when it was considered "helping the GC" to
explicitly null variables out.  Leaking OOP fields of null values will
bring that guidance back and "performance sensitive" users will start
zeroing the OOP fields before writing null.  Yuck.

And there will be cliffs in this design that are both implementation
and runtime hardware specific where assigning null won't leak because
the implementation is an indirection and others where it will leak.
That's a hard performance model for users to adapt to, especially as
our (implementers) ability to take advantage of non-atomic values
grows exposing the leak behaviour in code that will have already been
written. (ie: in the period between shipping this feature and the
implementations taking advantage of the freedom).

So where does that leave us?  Working out very careful state diagrams
to try to not tear the null bit while still zeroing OOPs?

We also need to think about what the memory model says about allowing
null checks to be commoned up.  My assumption is that any field access
is really two accesses:
* a read of the null bit
* a read of the real field
Sometimes we can do that in a single read, sometimes we can't.

Pseudo-source code will say things like:
class Holder {
   /*non-atomic nullable value */ DateTime dt;
}
Holder h = ....;

long date = h.dt.d;
long time = h.dt.t;

And this will be treated as though there are two null checks (just
like with references today):

bool isNull = h.dt.isNull;
if (isNull) throw NPE;
long date = h.dt.d;
bool isNull = h.dt.isNull;
if (isNull) throw NPE;
long time = h.dt.t;

And it would be nice to be able to common that into a single null
check followed by the reads of the field.  Is that legal?  And how far
can we float the read of the null bit from the field access it
protects?  Likewise, can we float the field read early so that we
privatized a whole copy of `dt` and have a consistent view of it, even
though it would be reading out of program order?

Whichever set of behaviours we pick here, the memory model will need a
careful review and update to ensure consistent behaviour across
implementations.

--Dan



More information about the valhalla-spec-experts mailing list