Addressing the full range of use cases
Dan Smith
daniel.smith at oracle.com
Tue Oct 5 00:04:07 UTC 2021
Here's a followup with some answers reflecting my own understanding and what we've learned at Oracle while investigating these ideas. (Presented separately because there's still a lot of uncertainty, and because I want to encourage focusing on the contents of the original mail, with this reply as a supplement.)
> On Oct 4, 2021, at 5:34 PM, Dan Smith <daniel.smith at oracle.com> wrote:
>
> Some questions to consider for this approach:
>
> - How do we group features into clusters so that they meet the sweet spot of user expectations and use cases while minimizing complexity? Is two clusters the right number? Is two already too many? (And what do we call them? What keywords best convey the intended intuitions?)
A "classic" and "encapsulated" pair of clusters seems potentially workable (better names TBD). Classic primitive classes behave as described in JEP 401—this piece is pretty stable. (Although some pieces, like the construction model, could be refined to better match their less class-like semantics.) Encapsulated primitive classes are always nullable and (maybe?) always atomic.
Nullability can be handled in one of two ways:
- Flush the previous mental model that null is inherently a reference concept. Null is a part of the value set of both encapsulated primitive value types and reference types.
- Encapsulated primitives are *always* reference types. They're just a special kind of reference type that can be optimized with flattening; if you want finer-grained control, use a classic primitive class. However, we often do the exercise of trying to get rid of the ".ref" type, only to find that there are still significant uses for a developer-controlled opt out of all flattening...
For migration, encapsulated primitive classes mostly subsume "reference-default" classes, and let us drop the 'Foo.val' feature. As nullable types, encapsulated primitive value types are source compatible replacements for existing reference types, and potentially provide an instant performance boost on recompilation. (Still to do, though: binary compatibility. There are some strategies we can use that don't require so much attention in the language. This is a complex enough topic that it's probably best to set it aside for now until the bigger questions are resolved.)
> - If there are knobs within the clusters, what are the right defaults? E.g., should atomicity be opt-in or opt-out?
Fewer knobs are better. Potentially, the "encapsulated"/"classic" choice is the only one offered. Nullability and atomicity would come along for the ride, and be invisible to users. *However*, performance considerations could push us in a different direction.
For the "encapsulated"/"classic" choice, perhaps "encapsulated" should be the default. Classic primitives have sharper edges, especially for class authors, so perhaps can be pitched as an "advanced" feature, with an extra modifier signaling this fact. (Everybody uses 'int', but most people don't need to concern themselves with declaring 'int'.)
Alternatively, maybe we'd prefer a term for "classic", and a separate term for "encapsulated"? (Along the lines of "record" and "enum" being special kinds of classes with a variety of unique features.)
> - What are the performance costs (or, in the other direction, performance gains) associated with each feature? For certain feature combinations, have we canceled out the performance gains over identity classes (and at that point, is that combination even worth supporting?)
Nullability:
Encapsulated primitive class types need *nullable Q types* in the JVM. A straightforward way to get there is by adding a boolean flag to the classes. This increases footprint in some cases, but is often essentially free. (For example: if the size of an array component must be a power of 2, boolean flags only increase the array size for 2 or so classes in java.time. Most have some free space.)
There are some other strategies JVMs could use to compress null flags into existing footprint. In full generality, this could involve cooperation with class authors ("this pointer won't be null"). But it seems like that level of complexity might be unnecessary—for footprint-sensitive use cases, programmers can always fall back to classic primitive classes.
Execution time costs of extra null checks for nullable Q types need to be considered and measured, but it seems like they should be tolerable.
Atomicity:
JVM support for atomicity guarantees seems more difficult—algorithms for ensuring atomicity above 64 bits tend to be prohibitively expensive. The current prototype simply gives up on flattening when atomicity is requested; not clear whether that's workable as the default behavior for a whole cluster of primitive classes. There are plenty of stack-level optimizations still to be had, but giving up on heap optimizations for these classes might be disappointing. (Can we discover better algorithms, or will hardware provide viable solutions in the near future? TBD...)
Alternatively, can we train programmers to treat out-of-sync values with the same tolerance they give to out-of-sync object state in classes that aren't thread safe? It seems bad that a hostile or careless third party could create a LocalDate for February 31 via concurrent read-writes, with undefined subsequent instance method behavior; but is this more bad than how the same third party could *mutate* (via validating setters) a similar identity object with non-final fields to represent February 31?
Migration:
As noted above, minimal performance costs in this approach, even when using the plain class name as a type. Legacy class files will continue to use L types, though, and those have some inherent limitations. (We've prototyped scalarizing of L types in certain circumstances, but you really need the Q signal for optimal performance.)
Overall:
Optimistically, even if pointers are the best implementation (for now) of heap storage for encapsulated primitive value types, there's a lot to be gained by stack optimizations, and those could well be enough to justify the feature.
If, pessimistically, the overall performance doesn't look good, it's worth asking whether we should tackle these use cases at all. But there's a risk that developers would misuse classic primitives if we don't provide the safer alternative. Could we effectively communicate "you're doing it wrong, just use identity"? Not sure.
More information about the valhalla-spec-observers
mailing list