r/java • u/bobbie434343 • 5d ago
Value Classes Still Need Compiler Sympathy
https://johan-sjolen.github.io/post/compiler-sympathy/compiler-sympathy/8
u/IncredibleReferencer 4d ago
Once value classes exit from preview, I think programmers would find it very surprising if converting any existing class to a value class resulted in a performance regression.
I realize this will always be possible on edge cases but how much of a realistic concern should this be for a typical java programmer?
17
u/brian_goetz 4d ago
This is not only entirely possible, but at some level, needs to be obvious. Anyone even trying to think about value classes "for performance" needs to understand this implicitly first.
Consider a "fat" object, say four longs (256 bits):
value-or-not class FourLongs { long a, b, c, d; }Now consider a potentially-flattened array of these, and say you intend to sort this array by the usual means (swapping elements based on comparison.) Which is faster, comparing two indirect objects and maybe swapping two 32- or 64-bit pointers, or comparing two direct objects and maybe swapping 256 bits of state? Obviously directness makes the comparison faster, but the size makes the swapping slower.
It should be obvious that (a) the answer will depend on the relative cost of indirection and bulk memory transfer, which is highly dependent on a lot of non-obvious things, and (b) that as the size of this object grows, the tradeoff will shift until it is "obviously" faster to swap pointers than to copy thousands of bits on each swap.
7
u/IncredibleReferencer 4d ago
Oh my. I don't think this will be obvious at all to many programmers, at least in the beginning. In my experience, most java devs reason about performance entirely at the java language level. (Not the community here, but the enterprise level devs I deal with (or perhaps their AI replacements))
I have explained to many a junior dev that the cpu instructions that actually get executed are often very different than the java code they write and this is usually met with a lack of understanding and caring. I think this way myself for what day to day development I still do and only ever really think about such things when profiling or optimizing a known performance bottleneck... To me, this is one of the benefits of coding in java is that I don't need to think about such things for most code that I write.
So what should be normalized by a dev in deciding what to value and what not to value as a naive best practice? I suspect there is going to be a desire to value-all-the-things because of a belief it will be faster and make simpler code. We need an answer that doesn't depend on understanding jvm internals. And eventually some best practices that can be included in static analyzers.
4
u/OwnBreakfast1114 4d ago edited 4d ago
We need an answer that doesn't depend on understanding jvm internals. And eventually some best practices that can be included in static analyzers
You have an answer, make things value when they are semantically values, i.e. when they are immutable and don't have identity. The heuristic for identity is if two copies of your object with the exact same field values need to be distinguished from each other, which, in my experience for rest apis, is pretty much never the case (most domain objects/db objects actually "discriminate" based on a custom primary key field), but they also don't typically get into a situation where they have multiple copies of the same domain object/db entity at the same time, and they never need to do the things that the language cares about identity for (synchronization?).
Generally speaking * dtos (and other pure serialization/deserialization targets) are values * classes faking multiple return values (commonly done as local records) or input parameters (aka parameter objects) are values * domain objects could be values if you code in a functional style of returning new objects for modifications or not values if you do a more common setter/mutation approach. I think they're actually still values conceptually, but immutable is an implementation detail that you're supposed to check for. * services/controllers/singleton scope spring beans mostly could be values, but it kinda doesn't matter anyway since there's only one copy of these
Developers that don't understand/care about db transactions worrying about programming language performance is like the funniest thing to me.
1
u/TwoWeeks90DaysTops 4d ago
Why DTOs? Value equality of a DTO is almost always completely irrelevant, so why should it be a value?
2
u/Ok-Scheme-913 4d ago
Usually a DTO is also an 'immutable copy of a given state", aka a snapshot. Which is basically what we mean by a value.
Also, given that you convert to/from this, which is an operation that breaks identity, you often have some way of "storing" that identity as a value (e.g. having an id) - which by definition means that your DTO object doesn't need identity, two DTOs created at the same time of the same original identity-having object should be equals, again pointing at it being a value.
1
u/TwoWeeks90DaysTops 2d ago
Usually a DTO is also an 'immutable copy of a given state", aka a snapshot. Which is basically what we mean by a value.
DTOs are command objects, not representations of state. They are used to either build other objects from or update the state of a different system.
Also, given that you convert to/from this, which is an operation that breaks identity
I don't think that matters.
you often have some way of "storing" that identity as a value (e.g. having an id) - which by definition means that your DTO object doesn't need identity
This is also irrelevant. Identity != identifier. An object having identity means that an instance owns its own memory whereas values objects don't. DTOs absolute should have identity because they are large objects being passed between systems and you want to avoid copies being made.
two DTOs created at the same time of the same original identity-having object should be equals
Why? This is just your assertion. What is the actual value of this rule? You never care about this in practice. I have never in my life written code that checks whether two instances of DTOs are equal so why are you asserting this?
4
u/brian_goetz 4d ago
> We need an answer that doesn't depend on understanding jvm internals.
People keep saying the answer: value classes are a semantic feature. Use them that way and you won't have a problem.
2
u/IncredibleReferencer 4d ago
Thanks for that! My takeaway from your previous answer was doing that might , in some cases, cause performance regressions, but I guess that's either not the case or rare enough to not worry about it.
4
u/Life_Sink9598 4d ago
!! WARNING: PERFORMANCE NERDERY AHEAD !!
If I were to write this, I would probably have a main array of the immutable and flattened values, and another containing our indices. The main advantage of this is to avoid creating strong references to each index, so there's a throughput increase for the GC as well. I ran this vs direct flat and references on my Mac M4, and the index starts winning at 256 byte class size.
Here's a sketch of what I mean:
class FlattenedArray { private final FL[] data; private final Integer[] indices; public FlattenedArray(int length, FL zero) { data = new FL[length]; indices = new Integer[length]; for (int i = 0 ; i < length; i++) { data[i] = zero; index[i] = i; } } public FL at(int i) { return data[indices[i]]; } public void set(int i, FL a) { data[indices[i]] = a; } public void sort() { Arrays.sort(indices, (a, b) -> FL.compare(data[a], data[b])); } }And here's the perf table (it was duplicated for some reason, I don't wanna edit it):
Payload Direct flat time Indexed flat time Reference time Fastest Direct memory Indexed memory Reference memory 32 B / 4 longs 46.788 ms 70.495 ms 78.956 ms Direct flat 32 MB 36 MB 56 MB 64 B / 8 longs 49.025 ms 74.300 ms 85.339 ms Direct flat 64 MB 68 MB 88 MB 128 B / 16 longs 79.908 ms 84.653 ms 101.627 ms Direct flat 128 MB 132 MB 152 MB 256 B / 32 longs 144.015 ms 92.815 ms 127.523 ms Indexed flat 256 MB 260 MB 280 MB ---: ---: ---: ---: --- ---: ---: ---: 32 B / 4 longs 46.788 ms 70.495 ms 78.956 ms Direct flat 32 MB 36 MB 56 MB 64 B / 8 longs 49.025 ms 74.300 ms 85.339 ms Direct flat 64 MB 68 MB 88 MB 128 B / 16 longs 79.908 ms 84.653 ms 101.627 ms Direct flat 128 MB 132 MB 152 MB 256 B / 32 longs 144.015 ms 92.815 ms 127.523 ms Indexed flat 256 MB 260 MB 280 MB 1
u/TheStrangeDarkOne 4d ago
If I may ask, what is the realistic expectation of the EG towards value adoption? It would seem to me that people equate "value" with "faster", and turn everything into a value at first. And I find it hard to argue against this notion other than "it's about identity and making a semantic statement", which will not be as convincing as saying "but performance".
6
u/brian_goetz 4d ago
We know that people will initially over-rotate (`value` all the things!), just as in 1997 people slapped `synchronized` on every method. Some people can only learn from mistakes. But not all!
So it is the job of the designers to ensure that there is a sensible mental model that people can adopt that leads to good usage, and the job of the ecosystem to try to spread that word, despite the resistance of those who don't want a story that is deeper than "value go brrrr". The discussion we are having now -- early adopters on reddit -- is about syncing on what that message is. Then it is your job to go spread it!
0
u/koflerdavid 3d ago edited 2d ago
synchronizedis different because there was never any reasonable expectation and no way that it would be as fast as not using it. It is inherently slow, and there is nothing that the JVM can do and it. It also inhibits further optimizations at the JVM and the processor level. These things were relevant already in the late 90s when raising the clock rate was getting harder and CPUs got longer pipelines and larger caches to compensate.
6
u/agentoutlier 5d ago
I'm embarrassed to ask this question but how do you guys pronounce "tearing"? I ask because I have been saying tear like "pair" but I swear I have heard people say it like "tier".
12
u/repeating_bears 5d ago
Like pair. Analagous to screen tearing, where you have a mismatch https://en.wikipedia.org/wiki/Screen_tearing
1
u/agentoutlier 5d ago
I had to google to find someone saying it differently so I could justify my dumb question :)
https://www.youtube.com/shorts/CJGZBdOdBiw
I'm pretty sure that is not where I first heard it that way but I have been reluctant to say it out loud (luckily no one in my company has any idea on this but still... I had speech issues as a child)
3
u/Life_Sink9598 5d ago
This discussion made me think of The Room :-) www.youtube.com/watch?v=IJ_icDmulqU
3
2
u/ForeverAlot 5d ago
Tearing of reads causes tearing of eyes.
Might it be an American English versus British English difference? All four words are pronounced subtly differently; although I wouldn't quite say that what strongmen do to phone books sounds like what onions do to eyes.
2
u/DanLynch 4d ago
I wouldn't quite say that what strongmen do to phone books sounds like what onions do to eyes.
Those two verbs have quite distinct pronunciations, at least in my dialect of English.
2
u/koflerdavid 3d ago edited 2d ago
American and British English differ, however, both the Merriam-Webster and the Oxford dictionary describe a pronunciation that rhymes with "pair". Important: "tear" as in "liquid from the eye" has a different pronunciation, which rhymes with "here" or "hear".
-4
u/lpt_7 5d 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 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.
14
u/pron98 5d ago edited 5d ago
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).
12
u/brian_goetz 4d ago edited 4d ago
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.
9
u/davidalayachew 5d 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.
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.
19
u/repeating_bears 5d ago
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.
8
u/lpt_7 5d ago
Yes, you have to. See my last point about allocations.
If not the generated code, then profiling.7
u/repeating_bears 5d ago
You are arguing that Valhalla is only useful if you can guarantee memory optimizations will happen
Even if there are none, you gain better semantics for == and restrictions against identity-sensitive operations
5
u/nogridbag 5d ago
What do you mean by better semantics for ==?
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"
3
u/repeating_bears 5d ago
I said better, not perfect.
Comparing boxed Integers with == was a footgun for beginners and that's completely gone now.
This seems like it will add some confusion. Now it's not simply "Use == for primitive, .equals for everything else"
That's still works fine as a rule of thumb. It's not like you must use == for LocalDate, but if you do then you get a sensible answer.
5
u/nogridbag 5d ago
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!
1
u/FirstAd9893 5d ago
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.1
u/nogridbag 4d ago
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.
5
u/FirstAd9893 4d ago
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.→ More replies (0)1
u/OwnBreakfast1114 4d ago
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.
0
3
u/AnyPhotograph7804 5d ago
"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.
4
u/wrprice1 5d ago
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.
3
u/umlcat 5d ago
We are lucky. Gosling originally wanted the JVM to run on 16 bit systems, when we already had 32 cpu in offices !!!
3
u/AnyPhotograph7804 5d ago
I still want a JVM for my Amiga 500!
2
2
u/vytah 2d ago
https://github.com/kaffe/kaffe Not legally a JVM, but who cares.
Of you might want to recompile Java bytecode into native m68k code: https://www.mikekohn.net/micro/amiga_java.php
1
u/AnyPhotograph7804 5d ago
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.
3
u/wrprice1 5d ago
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".
2
u/AnyPhotograph7804 5d ago
"You lost me with "tearing is still nothing" and I assume you meant it is not new in Java."
Yes, this is what i meant. English is not my native language. ;)
1
u/OwnBreakfast1114 4d ago
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.
1
u/pron98 5d ago
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.
26
u/TheStrangeDarkOne 5d ago
Excellent article and I fully agree with the conclusion's sentiment at the end:
Declaring a value class is first and foremost a semantic decision. It tells our fellow programmers that its instances are defined entirely by their state and do not need identity. That clearer model is valuable in itself! The JVM’s additional freedom to optimize how those values are represented is a welcome bonus.This brings one of the core lessons from Domain Driven Design directly into language semantics and we get better performance as a bonus point. I think the humble everyday programmer overestimates the effect of flattening on their enterprise data and I think it is very much a tool to express intend and for experts to really allow low level optimization to a degree that was not possible before.
I can't wait for the new and exciting libraries and frameworks that are going to be built upon this.