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.sm...@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.

Reply via email to