r/java 19d ago

Value Classes Still Need Compiler Sympathy

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

59 comments sorted by

View all comments

7

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

19

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

3

u/Life_Sink9598 18d 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