Finding the spirit of L-World
Brian Goetz
brian.goetz at oracle.com
Mon Jan 7 17:21:26 UTC 2019
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.
# Finding the Spirit of L-world
L-World is, at heart, an attempt to unify reference objects and
values; they're unified under a common top type (`Object`), a common
basic type descriptor (`L`), and a common set of bytecodes (`aload` et
al.) The war cry for L-World should be, therefore, "Everything is an
Object". And users will be thrilled to see such a unification --
assuming we can live up to the high expectations that such a promise
sets.
By unifying references and values under a common type descriptor and
supertype, we gain significant benefits for _migration_ -- that
migrating a reference class to a value class does not break the ways
existing code refers to it.
By unifying under a common set of bytecodes, we gain significant
benefits for _specialization_; the method body output by the compiler
can apply equally to reference and value parameterizations, and all
specialization can be applied on the constant pool only.
If our war cry is "Everything is an Object", we need to ask ourselves
what behaviors uses should reasonably expect of all objects -- and
ensure that values and references alike conform to those behaviors.
## Object model
In Q-world, we struggled with the fact that there was no true top
type, but most code was written as if `Object` were the top type. This
was trying to square a circle; the options for introducing a new top
type in Q-world were not good (an `Any` superclass provided the
desired unification but a woefully confusing cost model; an
`Objectible` interface shared between `Object` and values would set
off a snail stampede to migrate libraries to use `Objectible` as the
new fake top), but having multiple roots would have further
exacerbated the pain of the existing bipartite type system.
L-world offers us an out; it makes `Object` a true top type (save for
primitives -- but see "Poxing", below), so existing code that deals
with `Object` can immediately accept values (save for totality -- but
see "Totality", below) without requiring disruptive migration.
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 { ... }
```
We would enforce that `RefObject` is only extended by classes that do
not have the `ACC_VALUE` bit, that `ValObject` is only extended by
classes that do have the `ACC_VALUE` bit, and that classes that claim
to extend `Object` are implicitly reparented according to their
`ACC_VALUE` bit. (Actually, in this scheme, we can ditch the
`ACC_VALUE` bit entirely; at load time, we just look at the
superclass, and if it's `ValObject`, its a value class, otherwise
it's a reference class.)
Bringing ref-ness and val-ness into the type system in this way has
many benefits:
- It reinforces the user's understanding of the relationship between
values and references.
- It allows us to declare methods or fields that accept any object,
reference objects only, or value objects only, using existing
concepts.
- It provides a place to declare ref-specific or val-specific methods,
and ref-specific or value-specific implementations of `Object`
methods. (For example, we could implement `Object::wait` as a final
throwing method in `ValObject`, if that's the behavior we want).
- It allows us to express ref-ness or val-ness as generic type
bounds, as in `<T extends RefObject>`.
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>`).
## 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
```
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.)
This model eliminates the equality anomalies, and provides users with
an intuitive and sound basis for "same instance".
One might ask whether we really need to push this into `acmp`, or
whether we can leave `acmp` alone and provide a new API point for
substitutability, and have the compiler generate invocations of that.
While the latter is OK for new code, doing so would cause old code to
behave differently than new when operating on values (or interfaces
that may hold values), and may cause it to change its behavior on
recompile. If we're changing what `Object` means, and what `aload`
can operate on, we should update `acmp` accordingly.
#### `==` and `equals()`
Code that knows what type it is dealing with generally uses either
`==` or `equals()`, but not both; generic code (such as `HashMap`)
generally uses the idiom `a == b || a.equals(b)`. Such code _could_
fall back to just using `equals()`; this idiom arose as an
optimization to avoid the virtual method invocation, but the first
part can be dropped with no semantic loss.
As the cost of `==` gets higher, this optimization (as optimizations
often do!) may begin to bite back; the `equals()` implementation often
includes an `==` check as well. There are lots of things we can do
here, but it is probably best to wait to see what the actual
performance impact is before doing anything.
#### 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.)
#### 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.
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.
#### 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.
## Poxing
The Model 3 specializer focused on specializing generics over
primitives, not values (because we hadn't implemented values yet).
Many of the complexities we ran into in that exploration stemmed from
the accidental asymmetries between primitives and objects, including
irregularities in the bytecode set (single vs double slot, `if_icmpeq`
vs `dcmp` + `if`). Having unified references and values, it would be
really nice to unify primitives as well.
While we can't exactly do that easily, beacause of the intrusion to
the bytecode set, we may be able to come close, using a modified
boxing conversion. The problem with the existing boxing conversion is
that `Integer` is a heavy box with identity -- which means boxing is
expensive. There are two possible paths by which we could mitigate
this pain:
- Migrate `Integer` to be a value type;
- Create an alternate box for `int`, which is a value class (`ValInt`)
If we can box primitives to values, then we need not unify primitives
with objects -- we just insert boxing conversions in the places we
already do, and interpret specializations like `List<int>` to mean
"List of int's box".
Migrating `Integer` to be a value may seem the obvious move, but it is
fraught with compatibility constraints -- there is tons of legacy code
that does things like locking on `Integer` or depending on it's
strange accidental identity. Perhaps if we could totalize locking and
remove the public box constructors, we could get there -- but this
is not a slam-dunk.
The alternative is creating a value box for primitives (a "pox") and
adjust the compiler's boxing behavior (when boxing to `Object` or an
interface, prefer the pox to the box). This too has some
compatibility concerns, such as code that deals in `Object` that
assumes that primitives are always boxed to legacy boxes. We may be
able to finesse this by a trick -- to teach `instanceof` and
`checkcast` of the relationship between boxes and poxes, so that code
like:
```
if (o instanceof Integer) {
Integer i = (Integer) o;
// use o
}
```
would work on both `Integer` and `int`'s pox (by saying "yes" in
`instanceof` and doing the conversion in `checkcast`.) This move,
while somewhat risky, could allow us to relegate the legacy boxes to
legacy, and eventually deprecate them. (We could then have methods
and intefaces on the poxes, and lift them to the primitives via
poxing, so that `int` could be seen to implement `Comparable<int>` and
you could call `compareTo()` on ints.) While this would not be a true
unification, it would come much closer than we are now.
Clearly, both alternatives are risky and require more investigation
-- but both have promising payoffs.
## 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.)
## Generics
In Q-world, because values and references were so different,
specializable generic classes had to be compiled with additional
constraints. For a specializable type variable `T`, we enforced:
- Cannot compare a `T` to `null`
- Cannot assign `null` to a `T`
- Cannot assign a `T` to `Object`
- Cannot assign a `T[]` to `Object[]`
- Cannot lock on a `T`
- Cannot `==` on a `T`
In L-world, the need for most of these can go away. Because
everything is an object, we can assign values to `Object`, and
`acmp_null` should work on all objects, so comparing with `null` is
OK. If we have array covariance, the array assignment restriction
goes away. If we totalize locking and equality, those restrictions go
away. The only restriction that remains is the assignment to `null`.
But now the VM can express the difference between nullable values and
non-nullable values, and we can express this in the source type system
with `Nullable`. So all the Q-world restrictions go away, and they
are replaced by an indication that a given type variable (or perhaps
an entire generic class) is erased or reifiable, and we treat erased
type variables as if they have an implicit `Nullable` bound. Then the
compile-time null-assignment restriction reduces to "does `T` have a
`Nullable` bound", and the restriction against instantiating an erased
generic class with a Q-type reduces to a simple bounds violation.
(There's lots more to cover on generics -- again, separate document.)
More information about the valhalla-spec-experts
mailing list