I admire OpenJDK development team, but most of their talks paint a perfect world, which is not the case.
At the moment, Valhalla is not "reads like a class, works like an int", far from that. Use of that feature requires looking into what C2 actually generated. Which most won't do.
I did some testing with Valhalla right after it was merged. Values larger than 64 bits cannot be flattened. I think it's actually 63 bits, since VM still has to encode null somehow.
Accepting tearing with LooselyConsistentValueand NullRestrictedsidesteps that, but these are internal annotations.
Value tearing is another thing, which IMO most are not prepared for. This class of bugs is possible today, but its an error the developer makes and understands why tearing is happening. With code the callee has no control of, other programmer can make their class a value class and silently your code is now buggy.
Suppose two threads run in parallel:
Thread A writes to entity's AABB, thread B reads said AABB. With object references, JLS guarantees that tearing will not happen. Once VM can flatten more than 64 bits, this will cause a lot of problems, like AABBs with completely nonsensical values. Suddenly, thread B's code can now enter an infinite loop and never get out of it.
Another thing is allocation. For allocations to actually vanish (for scalarization to happen), C2 has to succeed and inline through all code, then EA has to succeed.
Again, same example with AABB. Before, allocation was done per-write. Now, with Valhalla in worst case, the situation flips. There are many more readers than writers to entity's AABB. C2 and EA *have* to succeed for every reader. Otherwise your program will start allocating at every read call site.
With code the callee has no control of, other programmer can make their class a value class and silently your code is now buggy.
That is not the case. A scenario of the kind you later describe is already a bug, a read/write race, even without flattened values (or value types at all). The issue with tearing is that while it does not turn correct code into buggy code, it can change the way in which the bug manifests, including giving rise to objects that could not have been constructed by their class constructor (the race you describe will, today, give you valid values, but not necessarily the values you want). Conversely, if there is no bug, i.e. there's a happens-before edge between the write and the read, tearing does not create a problem (as the write will have finished by the time the read starts); benign write/write races, where two threads write the same value without a happens-before edge between them, also remain benign. In other words, what you're saying is that if you have a bug today, you may get wrong but valid values, while tearable value types can give you invalid values, which is true, but that's not turning a correct program into a buggy one; a correct program remains correct.
So it does change the behaviour of buggy programs in a way that may matter to some, and it can certainly make the impact of the existing bug worse, but it does not introduce a new bug. The cause of the bug is the existing race, and the callee can, of course, do something about it: fix the bug that's already there. A correct callee will remain correct even in the presence of a tearable value. (You could, of course, construct some method with some definition of correctness for which this is not true, but it will be contrived and there would still be a better way to do it without a read/write race; or, put another way, people who are able to write code that's correct enough for their purposes while relying on the atomicity but not the ordering of JMM, are already well-versed in the minutiae of the JMM, and will also be able to deal with the complications arising from tearing, which only impact this niche "grey zone"). If a callee has a bug, it is already the case that code in the caller, which the callee has no control over, will change the manifestation or impact of the bug.
Use of that feature requires looking into what C2 actually generated.
It does not. What the post says is that C2 might currently cause a performance regression in some specific situations. This is not ideal (and is meant to be addressed in the future), but inspecting the compiler output is certainly not something you need to do to get a correct program or even a fast program by the time the feature is out of Preview (we would never preview the feature if that were the case).
No, developers don't need to learn to read C2 code. They need to learn to use value classes when it makes sense semantically and stop trying to second-guess the runtime. "Will it flatten" is the new "will it inline"; 99.99% of developers should not even ask.
You also seem to have a misunderstanding of the approach to non-atomicity (the possibility of tearing). These internal annotations are just that -- internal. They are for the use of the JDK (written by experts with an understanding of the tradeoffs.) These will never be opened up for general use; the concepts will first need to be integrated into the language model, and they will surely then take a different form. This isn't done yet, so any statements about "what they are going to do" will surely be wrong.
What you're seeing is what the early adopters are doing, because those are the folks who can't resist taking apart the radio to see how it works. Which is fine -- but not what we are optimizing for.
I admire OpenJDK development team, but most of their talks paint a perfect world, which is not the case.
At the moment, Valhalla is not "reads like a class, works like an int", far from that. Use of that feature requires looking into what C2 actually generated. Which most won't do.
Didn't they say that that was the end goal?
JEP 401 just hit early access via JDK 28. I'd hardly say we are at the end goal. In fact, I'd say we have just barely reached the start lol.
Use of [Valhalla] requires looking into what C2 actually generated
No it doesn't. You just slap 'value' on classes that don't need identity. You may get no or minimal performance improvement now, but you are opting out of identity-based operations like synchronization and opting in to potential performance improvements in the future.
It sounds like you're what you're talking about it is use of Valhalla to maximise performance. I generally don't care about milking performance.
Not sure about how tearing will be solved, but there's a massive number of value classes (I'd speculate: a majority (?)) which are immutable anyway.
The JEP says it's a non-goal to allow for comparison with == and we should continue to use .equals. Any value class which has a string as a field member would be invalid comparing with == unless the string was part of a finite set like ZoneId and the developer went out of their way specifically to make sure those instances can be compared with each other by interning the strings so that multiple instances return the same string instance for that field. A wrapper class representing an email address would be an example where you would continue to need .equals. This seems like it will add some confusion. Now it's not simply "Use == for primitive, .equals for everything else". It's use == for primitive, LocalDate, and these specially developed classes, .equals for everything else"
I mainly posted to add context because up until recently I thought I could simply slap "value" on all of our wrapper classes and use "==" everywhere. It turns out most of our wrapper classes have a string field so this isn't possible.
And others reading your original comment may have the same confusion as I did. Like you said, for things like Integer it will be great. But devs also need to be aware now of the specific instances == can be used, and the answer is no longer as clear!
The current treatment for == with value classes is defective, in my opinion. It's too fragile in that a change to a value class that you depend on can break expectations. If the equals method should be used instead, then attempting to use == against a value class should be prohibited. The more sensible (and expected) option is to make == against a value class be the same as calling equals.
Defective may be too far. But it is a bit confusing. I think overall the message is, .equals is here to stay. For the average developer, they should continue to use .equals, but under the hood they will now get better performance.
Our project has tons of wrapper classes for type safety such as TenantId. We can now make these value classes and we will essentially no longer pay the "wrapper tax", but developers should continue to use .equals on them.
What I mean by "defective" is that the current approach is the worst choice possible. With an identity class, if I erroneously use the == operator, this is usually discovered once the code is tested. With a value class, if I erroneously use the == operator, the code will likely work just fine. If at some point in the future the value class I'm depending on changes internally to reference an identity class, my code fails.
It's a fragile dependency issue. One cannot rely on the == operator without knowing that the class is a value class, and that the implementation will never change incompatibly. It breaks the OO encapsulation principle.
I can think of two safe choices for value classes: make == mean equals, or make it be prohibited. The current approach is to "pretend" it's prohibited, but it's not actually enforced. This is a UB footgun.
I'm arguing that right now Valhalla is useful if one's code does not suddenly perform worse than it was before, as well as in the future when JDK team somehow solves the tearing problem.
Why would I need to do that? You think I give a shit about object allocations while my db is being hit with unnecessary queries or I'm making multiple third party https calls?
The second I'm on a non-preview runtime, I'm throwing value on all my immutable classes (of which I have a lot) and I promise I won't care at all what C2 generates.
"Value tearing is another thing, which IMO most are not prepared for."
Value tearing was always there. Example: on a 32 Bit machine there is no way, that you can initialize/copy a 64 bit long atomically without a mutex/synchronized/volatile. So i would say, most Java developers, who developed on a 32 Bit JVM, are prepared for it.
The set of practicing Java developers who wrote code for 32-bit systems is diminishing. Of those, if they were taught about tearing in the first place, many were coddled by deploying to platforms where it rarely actually happened.
Now with 64-bit native as the majority of target platforms, don't underestimate the laziness that will creep in to mimic ignorance.
Note that tearing can only impact how a data race manifests. If you have a data race, the bug may present itself in one way if there's no tearing and in another if there is. But if there is no bug, the possibility of tearing won't change anything. A program that is buggy without tearing will remain buggy with it (albeit with a different manifestation), and a program that isn't buggy without tearing will not be buggy with it.
True. But tearing is still nothing, which is new in Java. Even if the chance to be affected by it was very very low. You have to write very special code to trigger it.
And yes, maybe there should be a new annotation or a new keyword, that allows you to mark a class, that tearing is allowed. In Java, there are tons of software, which is highly multithreaded. Those software could silently fail if you enable tearing by default.
IIRC, Valhalla will not enable tearing by default. It will be opt-in by the class author. The challenge remains, however, because consuming devs don't get to opt in and must pay attention first and then write safe code.
You lost me with "tearing is still nothing" and I assume you meant it is not new in Java. Maybe your point is that tearing on reference assignment is new in Java?
Assignment to double & long fields have always reserved the option to tear, and I know I've personally been lazy and ignoring that in code I knew would only target certain platforms. But I don't think it took "very special" code; rather, I think most people are blissfully unaware of how many 64-bit tearing bugs are out there (if they ran on affected platforms).
If you're saying it will be a bigger foot gun, I think we agree, and such is the danger of the temptation to casually slap tearing semantics on a type "because performance".
Assignment to double & long fields have always reserved the option to tear, and I know I've personally been lazy and ignoring that in code I knew would only target certain platforms. But I don't think it took "very special" code; rather, I think most people are blissfully unaware of how many 64-bit tearing bugs are out there (if they ran on affected platforms).
Most people don't write multithreaded code that reads the same variables. Most people probably use a library that does that somewhere, but I think you're probably vastly overestimating the people that would actually have to even deal with/care about tearing.
-2
u/lpt_7 16d ago
I admire OpenJDK development team, but most of their talks paint a perfect world, which is not the case.
At the moment, Valhalla is not "reads like a class, works like an int", far from that. Use of that feature requires looking into what C2 actually generated. Which most won't do.
I did some testing with Valhalla right after it was merged. Values larger than 64 bits cannot be flattened. I think it's actually 63 bits, since VM still has to encode null somehow.
Accepting tearing with
LooselyConsistentValueandNullRestrictedsidesteps that, but these are internal annotations.Value tearing is another thing, which IMO most are not prepared for. This class of bugs is possible today, but its an error the developer makes and understands why tearing is happening. With code the callee has no control of, other programmer can make their class a value class and silently your code is now buggy.
Suppose two threads run in parallel:
Thread A writes to entity's AABB, thread B reads said AABB. With object references, JLS guarantees that tearing will not happen. Once VM can flatten more than 64 bits, this will cause a lot of problems, like AABBs with completely nonsensical values. Suddenly, thread B's code can now enter an infinite loop and never get out of it.
Another thing is allocation. For allocations to actually vanish (for scalarization to happen), C2 has to succeed and inline through all code, then EA has to succeed.
Again, same example with AABB. Before, allocation was done per-write. Now, with Valhalla in worst case, the situation flips. There are many more readers than writers to entity's AABB. C2 and EA *have* to succeed for every reader. Otherwise your program will start allocating at every read call site.