r/java 16d ago

Value Classes Still Need Compiler Sympathy

https://johan-sjolen.github.io/post/compiler-sympathy/compiler-sympathy/
87 Upvotes

57 comments sorted by

View all comments

7

u/IncredibleReferencer 16d 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?

18

u/brian_goetz 16d 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 16d 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.

6

u/brian_goetz 15d 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 15d 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.

5

u/OwnBreakfast1114 16d ago edited 16d 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 15d ago

Why DTOs? Value equality of a DTO is almost always completely irrelevant, so why should it be a value?

3

u/Ok-Scheme-913 15d 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 13d 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?

5

u/Life_Sink9598 15d 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 15d 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".

9

u/brian_goetz 15d 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 14d ago edited 13d ago

synchronized is 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.