Do Java 8 streams produce slower code than plain imperative loops?
Asked Answered
L

3

11

There are too much hype about functional programming and particularly the new Java 8 streams API. It is advertised as good replacement for old good loops and imperative paradigm. Indeed sometimes it could look nice and do the job well. But what about performance?

E.g. here is the good article about that: Java 8: No more loops Using the loop you can do all the job with a one iteration. But with a new stream API you will chain multiple loops which make it much slower(is it right?). Look at their first sample. Loop will do not walk even through the whole array in most cases. However to do the filtering with a new stream API you have to cycle through the whole array to filter out all candidates and then you will be able to get the first one.

In this article it was mentioned about some laziness:

We first use the filter operation to find all articles that have the Java tag, then used the findFirst() operation to get the first occurrence. Since streams are lazy and filter returns a stream, this approach only processes elements until it finds the first match.

What does author mean about that laziness?

I did simple test and it shows that old good loop solution works 10x fast then stream approach.

public void test() {
    List<String> list = Arrays.asList(
            "First string",
            "Second string",
            "Third string",
            "Good string",
            "Another",
            "Best",
            "Super string",
            "Light",
            "Better",
            "For string",
            "Not string",
            "Great",
            "Super change",
            "Very nice",
            "Super cool",
            "Nice",
            "Very good",
            "Not yet string",
            "Let's do the string",
            "First string",
            "Low string",
            "Big bunny",
            "Superstar",
            "Last");

    long start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        getFirstByLoop(list);
    }
    long end = System.currentTimeMillis();

    System.out.println("Loop: " + (end - start));

    start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        getFirstByStream(list);
    }
    end = System.currentTimeMillis();

    System.out.println("Stream: " + (end - start));
}

public String getFirstByLoop(List<String> list) {

    for (String s : list) {
        if (s.endsWith("string")) {
            return s;
        }
    }

    return null;
}

public Optional<String> getFirstByStream(List<String> list) {
    return list.stream().filter(s -> s.endsWith("string")).findFirst();
}

Results was:

Loop: 517

Stream: 5790

BTW if I will use String[] instead of List the difference will be even more! Almost 100x!

QUESTION: Should I use old loop imperative approach if I'm looking for the best code performance? Is FP paradigm is just to make code "more concise and readable" but not about performance?

OR

is there something I missed and new stream API could be at least the same as efficient as loop imperative approach?

Latrell answered 14/1, 2018 at 11:0 Comment(7)
This is not how we benchmark Java. But yes, a Stream is a much more complex than a simple loop - it will be slower in this trivial case.Lavolta
This is a typical worse-than-worthless benchmark; it's worse than worthless because it produces a number which makes you think it means something. In reality, streams are often as fast as, sometimes faster than, and sometimes slower than traditional loops. And if you're iterating over data sets with fewer than millions of elements, it probably doesn't matter at all to overall program performance. Write the code that is clear and maintainable, and the performance is almost always going to be good enough.Featheredge
Guys, that is not a benchmark at all! It is the question. Show me please that I'm wrong and the performance of getFirstByStream is really better then getFirstByLoop in any trustful for you way and we will done. Or tell me please when streams are more performant and should be preferred for the sake of performance. thank you.Latrell
@Latrell thats the point, you would have to measure for the given scenario; then measure your app with a profiler and prove that streams are the weak point; and the last is that you should measure correctly and again this is not easy. But Brian Goetz responding to your question (the architect at Oracle/Java) is not enough for you?Vassar
What the author means with that laziness, is, that your understanding is fundamentally wrong. A stream solution will not chain multiple loops. Hence, all of your assumptions about performance are wrong too.Kamseen
One immediate problem with your timings is that the streams classes may have to be loaded and that is included in your timing.Discommodity
Streams incur dramatically increased overhead. Whether or not the OP's benchmark is valid, jumping to streams will increase runtime complexity over your own loop iteration. Where people seem to go astray is when comparing streams to heavily customized Iterator class facilities (to allow things such as the "foreach" idiom, or otherwise), where the differences are less. The advantage to using streams is code maintainability and time to market, but if speed is what you want, grow your own loop.Roselani
D
13

QUESTION: Should I use old loop imperative approach if I'm looking for the best code performance?

Right now, probably yes. Various benchmarks seem to suggest that streams are slower than loops for most tests. Though not catastrophically slower.

Counter examples:

It is possible to do equivalent things with loops, you can't do it with just loops.

But the bottom line is that performance is complicated and streams are not (yet) a magic bullet for speeding up your code.

Is FP paradigm is just to make code "more concise and readable" but not about performance?

Not exactly. It is certainly true that the FP paradigm is more concise and (to someone who is familiar with it) more readable.

However, by expressing the using the FP paradigm, you are also expressing it in a way that potentially could be optimized in ways that are much harder to achieve with code expressed using loops and assignment. FP code is also more amenable to formal methods; i.e. formal proof of correctness.

(In the context of this discussion of streams, "could be optimized" means in some future Java release.)

Dalton answered 14/1, 2018 at 11:28 Comment(0)
V
13

Laziness is about how elements are taken from the source of the stream - that is on demand. If there is needed to take more elements - they will, otherwise they will not. Here is an example:

 Arrays.asList(1, 2, 3, 4, 5)
            .stream()
            .peek(x -> System.out.println("before filter : " + x))
            .filter(x -> x > 2)
            .peek(System.out::println)
            .anyMatch(x -> x > 3);

Notice how each element goes through the entire pipeline of stages; that is filter is applied to one element at at time - not all of them, thus filter returns a Stream<Integer>. This allows the stream to be short-circuiting, as anyMatch does not even process 5 since there is no need at all.

Just notice that not all intermediate operations are lazy. For example sorted and distinct is not - and these are called stateful intermediate operations. Think about this way - to actually sort elements you do need to traverse the entire source. One more example that is not intuitive is flatMap, but this is not guaranteed and is seen more like a bug, more to read here

The speed is about how you measure, measuring micro-benchmarks in java is not easy, and de facto tool for that is jmh - you can try that out. There are numerous posts here on SO that show that streams are indeed slower (which in normal - they have an infrastructure), but the difference is not that big to actually care.

Vassar answered 14/1, 2018 at 11:16 Comment(4)
Difficult? Yes. At the very least with micro-benchmarks, it's critical that you run the test multiple times in a row. Java needs to "settle" down at first, and it's not confined to mere Class loading. Class loading is solved after the first go-through of most benchmarks, but any benchmark I concoct in a hurry keeps in mind that the first 3 or 4 runs produce nonsense, the other threads may need to calm down to a wait(), and if you're exercising the garbage collector (IMO, by definition, no longer a micro benchmark) then you're likely to never get a sense of where the bodies are buried.Roselani
@Roselani exactly why I said to use JMHVassar
"exactly why I said to use JMH" (?) Yes I know; I'm not saying you didn't say that. I was agreeing that creating a micro-benchmark is difficult, and was offering how it's possible to make one somewhat useful, or at least mitigate the issues invalidating it.Roselani
@Roselani I see. and agreed, JMH will take care of a lot of for you, also providing a lot of features also. It has become the de facto tool in the java world for micro-benchmarking.Vassar
D
13

QUESTION: Should I use old loop imperative approach if I'm looking for the best code performance?

Right now, probably yes. Various benchmarks seem to suggest that streams are slower than loops for most tests. Though not catastrophically slower.

Counter examples:

It is possible to do equivalent things with loops, you can't do it with just loops.

But the bottom line is that performance is complicated and streams are not (yet) a magic bullet for speeding up your code.

Is FP paradigm is just to make code "more concise and readable" but not about performance?

Not exactly. It is certainly true that the FP paradigm is more concise and (to someone who is familiar with it) more readable.

However, by expressing the using the FP paradigm, you are also expressing it in a way that potentially could be optimized in ways that are much harder to achieve with code expressed using loops and assignment. FP code is also more amenable to formal methods; i.e. formal proof of correctness.

(In the context of this discussion of streams, "could be optimized" means in some future Java release.)

Dalton answered 14/1, 2018 at 11:28 Comment(0)
A
2

This article's abstract summarizes the performance of streams:

We find that sequential streams in general are slower than imperative loops. However, stream performance heavily relies on how many elements are being processed, which is referred to as input size. For input sizes smaller than 100, most stream pipelines are several times slower than imperative loops. Meanwhile, for input sizes between 10 000 and 1 000 000, streams are on average only 39% to 74% slower than loops, and in some cases, they even slightly outperform them.

Academicism answered 2/12, 2023 at 14:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.