Java Holiday Calendar 2016 (Day 19): Speed Up Your Enums
The .values() method might seem speedy, but it actually creates a copy of your array, adding to your overhead. Doing a bit of extra work yourself will help in the long run.
Join the DZone community and get the full member experience.
Join For FreeToday's tip is about Enum performance. A large number of Java programmers think Enums have a very fast method called .values() that returns all the Enums. Heck, it is even an array being returned, so it should really be super fast? Well, not really...
Suppose we've declared an enum like this:
public enum Car {
TESLA, VOLVO, TOYOTA;
}
Then it turns out Java will generate an equivalent to the following under the hood:
public static Car[] values() {
return (Car[])$VALUES.clone();
}
This means each time we are calling values(), we are creating a copy of the internal VALUES array. The reason for this is that an array cannot be protected from being overwritten by user code. Thus, a new copy must be provided for each call to guarantee state integrity. The JVM can mitigate the problem under certain circumstances, but if we want to be absolutely sure no objects are created, we must implement our own equivalent to values(). This can be done like this:
public enum Car {
TESLA, VOLVO, TOYOTA;
private static final List<Car> VALUE_LIST = Stream.of(values())
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
public static List<Car> valuesAsList() {
return VALUE_LIST;
}
}
Note that the internal VALUE_LIST is unmodifiable and that this is important, or else we cannot expose it directly via the valuesAsList() method.
Now we can use our modified Enum like this with no performance overhead:
Car.valuesAsList().forEach(System.out::println);
Update
I ran the following JMH benchmark (thanks Joe for the proposal):
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Threads(Threads.MAX)
@Fork(3)
@State(Scope.Benchmark)
public class CarBenchmark {
@Benchmark
public Car[] valueBenchmark() {
return Car.values();
}
@Benchmark
public List<Car> valueAsListBenchmark() {
return Car.valuesAsList();
}
}
And it came out with the following result:
Result "valueBenchmark":
163315824.656 ±(99.9%) 23937389.480 ops/s [Average]
(min, avg, max) = (134687130.170, 163315824.656, 214846495.652), stdev = 22391048.973
CI (99.9%): [139378435.177, 187253214.136] (assumes normal distribution)
# Run complete. Total time: 00:01:03
Benchmark Mode Cnt Score Error Units
CarBenchmark.valueAsListBenchmark thrpt 15 1224047670.454 ± 49040744.220 ops/s
CarBenchmark.valueBenchmark thrpt 15 163315824.656 ± 23937389.480 ops/s
This indicates that valuesAsList() can be much faster than values(). In this case, it was almost ten times faster.
Update 2
If we look at a broader perspective than just getting the value() or getValueAsList() and actually consume the values we are iterating over, what is then the gain?
If we run the following tests:
private static final Consumer<? super Car> BLACKHOLE = c -> {};
@Benchmark
public void valueForEachBenchmark() {
for (Car car: Car.values()) {
BLACKHOLE.accept(car);
}
}
@Benchmark
public void valueAsListForEachBenchmark() {
Car.valuesAsList().forEach(BLACKHOLE);
}
Then valuesAsList() is still five times faster than values().
Result "valueForEachBenchmark":
177293926.439 ±(99.9%) 10702768.085 ops/s [Average]
(min, avg, max) = (155995378.714, 177293926.439, 197287067.497), stdev = 10011375.908
CI (99.9%): [166591158.355, 187996694.524] (assumes normal distribution)
# Run complete. Total time: 00:00:49
Benchmark Mode Cnt Score Error Units
CarBenchmark.valueAsListForEachBenchmark thrpt 15 706004496.271 ± 77237061.646 ops/s
CarBenchmark.valueForEachBenchmark thrpt 15 177293926.439 ± 10702768.085 ops/s
As the number of elements in the Enum increases, the valuesAsList() gets even better relative to values(). See the following graph:
X-axis: Number of Enum elements, Y-axis: operations per second. Higher is better.
N.B. Replacing the valueForEachBenchmark's for-each loop with a for (int i=0, i<values.length;i++) {...values[i]...} does not affect the performance noticable.
Follow the Java Holiday Calendar 2016 with small tips and tricks all the way through the winter holiday season.
Opinions expressed by DZone contributors are their own.
Comments