Thanks for writing this up Brian.  It clearly lists a number of the problematic areas that were identified as potentially needing re-validation.
 
The key questions are around the mental model of what we're trying to accomplish and how to make it easy (easier?) for users to migrate to use value types or handle when their pre-value code is passed a valuetype.  There's a cost for some group of users regardless of how we address each of these issues.  Who pays these costs?  Those migrating to use the new value types functionality?  Those needing to address the performance costs of migrating to a values capable runtime (JDK-N?).
 
One concern writ large across our response is performance.  I know we're looking at user model here but performance is part of that model.  Java has a well understood performance model for array access, == (acmp), and it would be unfortunate if we damaged that model significantly when introducing value types.
 
Is this a fair statement of the projects goals: to improve memory locality in Java by introducing flattenable data?  The rest of where we've gotten to has been working all the threads of that key desire through the rest of the java platform.  The L/Q world design has come about from starting from a VM perspective based on what's implementable in ways that allows the JVM to optimize the layout.
 
One of the other driving factors has been the desire to have valuetypes work with existing collections classes.  And a further goal of enabling generic specialization to allow those collections to get the benefits of the flattened data representations (ie: backed by flattened data arrays).
 
You made an important point when talking the ValObject / RefObject split - "This is probably what we'd have done if values had always been part of the Java platform".  I think we need to ask that same question about some of the other proposals and really look at if they're the choices we'd be making if values had always been part of the platform.
 
The other goal we discussed in Burlington was that pre-value code should be minimally penalized when values are introduced, especially for code that isn't using them.  Otherwise, it will be a hard sell for users to take a new JDK release that regresses their existing code.
 
Does that accurate sum up the goals we've been aiming for?
 
 
I’ve been processing the discussions at the Burlington meeting.  While I think we made a lot of progress, I think we fell into a few wishful-thinking traps with regard to the object model that we are exposing to users.  What follows is what I think is the natural conclusion of the L-World design — which is a model I think users can love, but requires us to go a little farther in what the VM does to support it.

<snip>

A sensible rationalization of the object model for L-World would be to
have special subclasses of `Object` for references and values:

```
class Object { ... }
class RefObject extends Object { ... }
class ValObject extends Object { ... }
```

 
Would the intention here be to retcon existing Object subclasses to instead subclass RefObject?  While this is arguable the type hierarchy we'd have if creating Java today, it will require additional speculation from the JIT on all Object references in the bytecode to bias the code one way or the other.  Some extra checks plus a potential performance cliff if the speculation is wrong and a single valuetype hits a previous RefObject only callsite.
 
How magic would these classes be in the VM?  Would things like jvmti's classfile load hook be sent for them?  Adding fields to Object or ValObject would grow all the ValueTypes loaded which would be expensive for the sweet spot of small values.
 


We can pull the same move with nullability, by declaring an interface
`Nullable`:

```
interface Nullable { }
```

which is implemented by `RefObject`, and, if we support value classes
being declared as nullable, would be implemented by those value
classes as well.  Again, this allows us to use `Nullable` as a
parameter type or field type, or as a type bound (`<T extends
Nullable>`).  
I'm still unclear on the nullability story.  None of the options previously discussed (memcmp to ensure its all 0, some pivot field, a special bit pattern, did I miss any?) are particularly attractive and all come with different levels of costs. Even the vulls? (convert "null value" to null reference on the stack) story has additional costs and complexity that leak throughout the system - ie: the gc will need to know about the null check to know whether to mark / update / copy the reference fields of the value type - and have knock-on affects to the equality discussion below.
 
Which model of nullability would map to this interface?
 
Do nullable values help meet the primary (flattenable data) or secondary goals (interop with collections)?  While they may help the second goal, I think they fail the first one.  Gaining collections at the cost of flattenability suggests we've missed our design center here.
 


## Totality

The biggest pain point in the LW1 model is that we're saying that
everything is an `Object`, but we've had to distort the rules of
`Object` operations in ways that users might find confusing.  LW1 says
that equality comparison, identity hash code, locking, and
`Object::wait` are effectively partial, but existing code that deals
in `Object` may be surprised to find this out.  Additionally, arrays
of reference objects are covariant with `Object`, but arrays of value
objects are currently not.  

#### Equality

The biggest and most important challenge is assigning sensible total
semantics to equality on `Object`; the LW1 equality semantics are
sound, but not intuitive.  There's no way we can explain why for
values, you don't get `v == v` in a way that people will say "oh, that
makes sense."  If everything is an object, `==` should be a reasonable
equality relation on objects.  This leads us to a somewhat painful
shift in the semantics of equality, but once we accept that pain, I
think things look a lot better.

Users will expect (100% reasonably) the following to work:

```
Point p1, p2;

p1 == p1  // true

p2 = p1
p1 == p2  // true

Object o1 = p1, o2 = p2;

o1 == o1  // true
o1 == o2  // true
```
We ran into this problem with PackedObjects which allowed creating multiple "detached" object headers that could refer to the same data.  While early users found this painful, it was usually a sign they had deeper problems in their code & understanding.  One of the difficulties was that depending on how the PackedObjects code was written, == might be true in some cases.  We found a consistent answer was better - and helped to define the user model.
 
In terms of values, is this really the model we want?  Users are already used to needing to call .equals() on equivalent objects.  By choosing the answer carefully here, we help to guide the right user mental model for some of the other proposals - locking being a key one. 


In LW1, if we map `==` to `ACMP`, they do not, and this will violate
both user intuition and the spirit of "everything is an object".  (If
everything is an object, then when we assign `o1 = p1`, this is just a
widening conversion, not a boxing conversion -- it's the same
underlying object, just with a new static type, so it should behave
the same.)

The crux of the matter is that interfaces, and `Object` (which for
purposes of this document should be considered an honorary interface)
can hold either a reference or a value, but we've not yet upgraded our
notion of interfaces to reflect this kind-polymorphism.  This is what
we have to put on a sounder footing in order to not have users fall
into the chasm of anomalies.  To start with:

  - A class is either a ref class or a value class.
  - `C implements I` means that instances of `C` are instances of `I`.
  - Interfaces are polymorphic over value and ref classes.

Now we need to define equality.  The terminology is messy, as so many
of the terms we might want to use (object, value, instance) already
have associations. For now, we'll describe a _substitutability_
predicate on two instances:

  - Two refs are substitutable if they refer to the same object
    identity.
  - Two primitives are substitutable if they are `==` (modulo special
    pleading for `NaN` -- see `Float::equals` and `Double::equals`).  
  - Two values `a` and `b` are substitutable if they are of the same
    type, and for each of the fields `f` of that type, `a.f` and `b.f`
    are substitutable.  

We then say that for any two objects, `a == b` iff a and b are
substitutable.  

This is an "everything is an object" story that users can love!
Everything is an object, equality is total and intuitive on objects,
interfaces play nicely -— and there are no pesky boxes (except for
primitives, but see below.)  The new concept here is that interfaces
abstract over refs and values, and therefore operations that we want
to be total on interfaces -- like equality -- have to take this seam
into account.

The costs come in two lumps.  The first is that if we're comparing two
objects, we first have to determine whether they are refs or values,
and do something different for each.  We already paid this cost in
LW1,  but here comes the bigger cost: if a value class has fields
whose static types are interfaces, the comparison may have to recur on
substitutability. This is horrifying for a VM engineer, but for users,
this is just a day at the office -- `equals` comparisons routinely
recur.  (For values known to (recursively) have no interface fields
and no floating point fields, the VM can optimize comparison to a flat
bitwise comparison.)
 
While the conceptual model may be clean, it's also, as you point out, horrifying.  Trees and linked structures of values become very very expensive to acmp in ways users wouldn't expect.
 
If we do this, users will build the mental model that values are interned and that they are merely fetching the same instances from some pool of values.  This kind of model will lead them down rabbit holes - and seems to give values an identity.  We've all seen abuses of String.intern() - do we want values to be subject to that kind of code?
 
The costs here are likely quite large - all objects that might be values need to be checked, all interfaces that have ever had a value implement them, and of course, all value type fields plus whatever the Nullability model ends up being.


#### Identity hash code

Because values have no identity, in LW1 `System::identityHashCode`
throws `UnsupportedOperationException`.  However, this is
unnecessarily harsh; for values, `identityHashCode` could simply
return `hashCode`.  This would enable classes like `IdentityHashMap`
(used by serialization frameworks) to accept values without
modification, with reasonable semantics -- two objects would be deemed
the same if they are `==`.  (For serialization, this means that equal
values would be interned in the stream, which is probably what is
wanted.)
 
By return `hashCode`, do you mean call a user defined hashCode function?  Would the VM enforce that all values must implement `hashCode()`?  Is the intention they are stored (growing the size of the flattened values) or would calling the hashcode() method each time be sufficient?
 

#### Locking

Locking is a difficult one.  On the one hand, it's bad form to lock on
an object that hasn't explicitly invited you to participate in its
locking protocol.  On the other hand, there is likely code out there
that does things like lock on client objects, which might expect at
least exclusion with other code that locks the same object, and a
_happens-before_ edge between the release and the acquire.  Having
locking all of a sudden throw `IllegalMonitorStateException` would
break such code; while we may secretly root for such code to be
broken, the reality is that such code is likely at the heart of large
legacy systems that are difficult to modify.  So we may well be forced
into totalizing locking in some way.  (Totalizing locking also means
totalizing the `Object` methods related to locking, `wait`, `notify`,
and `notifyAll`.)

There are a spectrum of interpretations for totalizing locking, each
with different tradeoffs:

 - Treat locking on a value as an entirely local operation, providing
   no exclusion and no happens-before edge.  Existing code will
   continue to run when provided with values, but may produce
   unexpected results.  
 - Alternately, treat locking on a value as providing no exclusion,
   but with acquire and release semantics.)  Wait and notify would
   still throw.  
 - Treat locking on a value as acquiring a fat lock (say, a global
   value lock, a per-type value lock, etc.)  This gives us exclusion
   and visibility, with a small risk of deadlock in situations where
   multiple such locks are held, and a sensible semantics for wait
   and notify (single notify would have to be promoted to `notifyAll`).
 - Treat locking on a value as acquiring a proxy lock which is
   inflated by the runtime, which assigns a unique lock to each
   distinguishable value.
 - Put lock-related methods on `ValObject`, whose defaults do one of
   the above, and allow implementations to override them.  
Note, lock methods on ValObject doesn't cover monitor{enter,exit}.


While nearly all of these options are horrifying, the goal here is
not to do something _good_, but merely to do something _good enough_
to avoid crushing legacy code.  

 
The only consistent answer here is to throw on lock operations for values.  Anything else hides incorrect code, makes it harder for users to debug issues, and leaves a mess for the VM.  As values are immutable, the lock isn't protecting anything.  Code locking on unknown objects is fundamentally broken - any semantics we give it comes at a cost and doesn't actually serve users.
 
By saying no to ==, we can avoid this problem as well as it becomes clear that you won't have the identical value in hand to lock on.

#### Array covariance

Currently, for any class `C`, `C[] <: Object[]`.  This makes
`Object[]` the "top array type".  If everything is an object, then an
array of anything should also be an array of `Object`.  

There are two paths to delivering on this vision: extend traditional
array covariance to value arrays (potentially making `aaload` sites
megamorphic), or moving in the direction of "Arrays 2.0" and  define a
specializable generic type `Array<T>` where the legacy arrays
implement  `Array<T>`, and require clients to migrate from `T[]` to
`Array<T>` before specializing their generic classes.
I've raised the performance concerns around this previously.  There's also some knock on effects here:
- additional code cache usage
- longer compiles due to extra work to constrain objects / interfaces to prove whether they are values or not
- performance cliffs when speculation is wrong
 


## Poxing
 
Note, checkcast doesn't return anything so conversions can't be done in checkcast.
 

## Migration

In both Q-world and L-world, we took care to ensure that for a value
class `C`, the descriptor `LC;` describes a subtype of `Object`.  This
is a key part of the story for migrating reference types to values,
since clients of `C` will describe it with `LC;` and we don't want to
require a flag day on migration.  In Q-world, `LC;` is the (nullable)
box for `C`; in L-world, it is a nullable `C`.  

This is enough that we can migrate a value-based class to a value and
_existing binary clients_ will not break, even if they stuff a null
into an `LC;`.  However, there are other migration compatibility
concerns which we need to take up (which I'll do in a separate
document.)

 
 
Asking the same question from ValObject/RefObject - are these choices the model we'd pick if designing Java from scratch today?  Does this model provide the flattened data performance the project is targeting?  And are the costs to non-value code reasonable?
 
--Dan
 

Reply via email to