Java: Gain Performance Using SingletonStream
Java streams sometimes create unnecessary overhead in your applications. Learn how to use SingletonStream objects and gain over tenfold performance in this post!
Join the DZone community and get the full member experience.
Join For FreeBackground
The Stream
library in Java 8 is one of the most powerful additions to the Java language ever. Once you start to understand its versatility and resulting code readability, your Java code-style will change forever. Instead of bloating your code with all the nitty and gritty details with for
, if
, and switch
statements and numerous intermediate variables, you can use a Stream
that just contains a description of what to do, and not really how it is done.
Some years ago, we had to make an API decision for a Java project: which return type should we select for the two fast local in-memory data cache methods with;
- A unique search key which returns either a value or no value
- A non-unique search key which returns any number of values (zero to infinity).
This was the initial idea:
Optional<T> searchUnique(K key); // For unique keys
Stream<T> search(K key); // For non-unique keys
But, we would rather have the two methods look exactly the same and both return a Stream<T>
. The API would then look much cleaner because a unique cache would then look exactly the same as a non-unique cache.
However, the unique search had to be very efficient and able to create millions of result objects each second without creating too much overhead.
Solution
By implementing a SingletonStream
that only takes a single element, and therefore, can be highly optimized compared to a normal Stream
with any number of elements, we were able to let both methods return a Stream
while retaining performance. The method searchUnique(K key)
would return an empty stream (Stream.empty()
) if the key was not found, and it would return a SingletonStream
with the value associated with the key if the key existed. We would get:
Stream<T> searchUnique(K key); // For unique keys
Stream<T> search(K key); // For non-unique keys
Great! We can eat the cookie and still have it!
The Implementation
The SingletonStream
is a part of the Speedment Stream ORM and can be viewed here on GitHub. Feel free to use Speedment and any of it's component in your own projects using the Speedment initializer.
The SingletonStream
is a good candidate for stack allocation using the JVM's Escape Analysis (read more on Escape Analysis in my previous posts here and here). The implementation comes in two shapes. if we set the STRICT value to true
, we will get a completely lazy Stream
, but the drawback is that we will lose the Singleton Property once we call some Intermediate Operations, like .filter(), map()
, etc. If we, on the other hand, set the STRICT
value to false
, the SingletonStream
will perform many of the Intermediate Operations eagerly and it will be able to return a new SingletonStream
, thereby retaining the Singleton Property. This will give better performance in many cases.
The solution devised here for reference streams could also easily be modified to the primitive incarnations of singleton streams. So, it would be almost trivial to write a SingletonIntStream
, a SingletonLongStream
and a SingletonDoubleStream
. Here is a SingletonLongStream.
It should be noted that the class could be further developed so it could support lazy evaluation while still being always high performant. This is a future work.
Performance
There are many ways one could test the performance of the SingletonStream
and compare it with a standard Stream
implementation with one element.
Here is one way of doing it using JMH. The first tests (count
) just counts the number of elements in the stream and the second tests (forEach
) does something with one element of a stream.
@Benchmark
public long singletonStreamCount() {
return SingletonStream.of("A").count();
}
@Benchmark
public long streamCount() {
return Stream.of("A").count();
}
@Benchmark
public void singletonStreamForEach() {
SingletonStream.of("A")
.limit(1)
.forEach(blackHole());
}
@Benchmark
public void streamForEach() {
Stream.of("A")
.limit(1)
.forEach(blackHole());
}
private static <T> Consumer<T> blackHole() {
return t -> {};
}
This will produce the following result when run on my MacBook Pro laptop:
...
Benchmark Mode Cnt Score Error Units
SingletonBench.singletonStreamCount thrpt 333419753.335 ops/s
SingletonBench.singletonStreamForEach thrpt 2312262034.214 ops/s
SingletonBench.streamCount thrpt 27453782.595 ops/s
SingletonBench.streamForEach thrpt 26156364.956 ops/s
...
That's a speedup factor over 10 for the count
operation. For the forEach
operation, it looks like the JVM was able to completely optimize away the complete code path for the SingletonStream
.
Test It
The complete test class is available here.
Conclusion
The SingletonStream
works more or less as an extended Optional
and allows high performance while retaining the benefits of the Stream
library.
You can select two versions of it by setting the STRICT value to your preferred stringency/performance choice.
The SingletonStream
could be further improved.
Published at DZone with permission of Per-Åke Minborg, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments