Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Java: Gain Performance Using SingletonStream

DZone's Guide to

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!

· Java Zone ·
Free Resource

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

Background

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.

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

Topics:
java ,singleton ,stream ,performance ,api

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}