Imperative Loop or Functional Stream Pipeline? Beware of the Performance Impact!

Spread the love

I like weird, yet concise language constructs and API usages

Because you’re evil.
— Nicolai Parlog (@nipafx) October 25, 2018

Yes. I am guilty. Evil? Don’t know. But guilty. I heavily use and abuse the java.lang.Boolean type to implement three valued logic in Java:
Boolean.TRUE means true (duh)
Boolean.FALSE means false
null can mean anything like “unknown” or “uninitialised”, etc.
I know – a lot of enterprise developers will bikeshed and cargo cult the old saying:
Code is read more often than it is written
But as with everything, there is a tradeoff. For instance, in algorithm-heavy, micro optimised library code, it is usually more important to have code that really performs well, rather than code that apparently doesn’t need comments because the author has written it in such a clear and beautiful way.
I don’t think it matters much in the case of the boolean type (where I’m just too lazy to encode every three valued situation in an enum). But here’s a more interesting example from that same twitter thread. The code is simple:

if (something) {
for (Object o : list)
if (something(o))
break woot;

throw new E();

Yes. You can break out of “labeled ifs”. Because in Java, any statement can be labeled, and if the statement is a compound statement (observe the curly braces following the if), then it may make sense to break out of it. Even if you’ve never seen that idiom, I think it’s quite immediately clear what it does.
If Java were a bit more classic, it might have supported this syntax:

if (something) {
for (Object o : list)
if (something(o))
goto woot;

throw new E();

Nicolai suggested that the main reason I hadn’t written the following, equivalent, and arguably more elegant logic, is because jOOQ still supports Java 6:

if (something &&
throw new E();

It’s more concise! So, it’s better, right? Everything new is always better.
A third option would have been the less concise solution that essentially just replaces break by return:

if (something && noneMatchSomething(list)
throw new E();

// And then:
private boolean noneMatchSomething(List list) {
for (Object o : list)
if (something(o))
return false;
return true;

There’s an otherwise useless method that has been extracted. The main benefit is that people are not used to breaking out of labeled statements (other than loops, and even then it’s rare), so this is again about some subjective “readability”. I personally find this particular example less readable, because the extracted method is no longer local. I have to jump around in the class and interrupt my train of thoughts. But of course, YMMV with respect to the two imperative alternatives.
Back to objectivity: Performance
When I tweet about Java these days, I’m mostly tweeting about my experience writing jOOQ. A library. A library that has been tuned so much over the past years, that the big client side bottleneck (apart from the obvious database call) is the internal StringBuilder that is used to generate dynamic SQL. And compared to most database queries, you will not even notice that.
But sometimes you do. E.g. if you’re using an in-memory H2 database and run some rather trivial queries, then jOOQ’s overhead can become measurable again. Yes. There are some use-cases, which I do want to take seriously as well, where the difference between an imperative loop and a stream pipeline is measurable.
In the above examples, let’s remove the throw statement and replace it by something simpler (because exceptions have their own significant overhead).
I’ve created this JMH benchmark, which compares the 3 approaches:
Imperative with break
Imperative with return
Here’s the benchmark

package org.jooq.test.benchmark;

import java.util.ArrayList;
import java.util.List;

import org.openjdk.jmh.annotations.*;

@Fork(value = 3, jvmArgsAppend = “-Djmh.stack.lines=3”)
@Warmup(iterations = 5, time = 3)
@Measurement(iterations = 7, time = 3)
public class ImperativeVsStream {

public static class BenchmarkState {

boolean something = true;

@Param({ “2”, “8” })
int listSize;

List list = new ArrayList();

boolean something() {
return something;

boolean something(Integer o) {
return o > 2;

public void setup() throws Exception {
for (int i = 0; i < listSize; i++) list.add(i); } @TearDown(Level.Trial) public void teardown() throws Exception { list = null; } } @Benchmark public Object testImperativeWithBreak(BenchmarkState state) { woot: if (state.something()) { for (Integer o : state.list) if (state.something(o)) break woot; return 1; } return 0; } @Benchmark public Object testImperativeWithReturn(BenchmarkState state) { if (state.something() && woot(state)) return 1; return 0; } private boolean woot(BenchmarkState state) { for (Integer o : state.list) if (state.something(o)) return false; return true; } @Benchmark public Object testStreamNoneMatch(BenchmarkState state) { if (state.something() && return 1; return 0; } @Benchmark public Object testStreamAnyMatch(BenchmarkState state) { if (state.something() && ! return 1; return 0; } @Benchmark public Object testStreamAllMatch(BenchmarkState state) { if (state.something() && -> !state.something(s)))
return 1;

return 0;

The results are pretty clear:

Benchmark (listSize) Mode Cnt Score Error Units
ImperativeVsStream.testImperativeWithBreak 2 thrpt 14 86513288.062 ± 11950020.875 ops/s
ImperativeVsStream.testImperativeWithBreak 8 thrpt 14 74147172.906 ± 10089521.354 ops/s
ImperativeVsStream.testImperativeWithReturn 2 thrpt 14 97740974.281 ± 14593214.683 ops/s
ImperativeVsStream.testImperativeWithReturn 8 thrpt 14 81457864.875 ± 7376337.062 ops/s
ImperativeVsStream.testStreamAllMatch 2 thrpt 14 14924513.929 ± 5446744.593 ops/s
ImperativeVsStream.testStreamAllMatch 8 thrpt 14 12325486.891 ± 1365682.871 ops/s
ImperativeVsStream.testStreamAnyMatch 2 thrpt 14 15729363.399 ± 2295020.470 ops/s
ImperativeVsStream.testStreamAnyMatch 8 thrpt 14 13696297.091 ± 829121.255 ops/s
ImperativeVsStream.testStreamNoneMatch 2 thrpt 14 18991796.562 ± 147748.129 ops/s
ImperativeVsStream.testStreamNoneMatch 8 thrpt 14 15131005.381 ± 389830.419 ops/s

With this simple example, break or return don’t matter. At some point, adding additional methods might start getting in the way of inlining (because of stacks getting too deep), but not creating additional methods might be getting in the way of inlining as well (because of method bodies getting too large). I don’t want to bet on either approach here at this level, nor is jOOQ tuned that much. Like most similar libraries, the traversal of the jOOQ expression tree generates stack that are too deep to completely inline anyway.
But the very obvious loser here is the Stream approach, which is roughly 6.5x slower in this benchmark than the imperative approaches. This isn’t surprising. The stream pipeline has to be set up every single time to represent something as trivial as the above imperative loop. I’ve already blogged about this in the past, where I compared replacing simple for loops by Stream.forEach()
Meh, does it matter?
In your business logic? Probably not. Your business logic is I/O bound, mostly because of the database. Wasting a few CPU cycles on a client side loop is not the main issue. Even if it is, the waste probably happens because your loop shouldn’t even be at the client side in the first place, but moved into the database as well. I’m currently touring conferences with a call about that topic:

In your infrastructure logic? Maybe! If you’re writing a library, or if you’re using a library like jOOQ, then yes. Chances are that a lot of your logic is CPU bound. You should occasionally profile your application and spot such bottlenecks, both in your code and in third party libraries. E.g. in most of jOOQ’s internals, using a stream pipeline might be a very bad choice, because ultimately, jOOQ is something that might be invoked from within your loops, thus adding significant overhead to your application, if your queries are not heavy (e.g. again when run against an H2 in-memory database).
So, given that you’re clearly “micro-losing” on the performance side by using the Stream API, you may need to evaluate the readability tradeoff more carefully. When business logic is complex, readability is very important compared to micro optimisations. With infrastructure logic, it is much less likely so, in my opinion. And I’m not alone:

In Spring Data, we consistently observed Streams of any kind (and Optional) to add significant overhead over foreach loops so that we strictly avoid them for hot code paths.
— Oliver Drotbohm (@odrotbohm) October 29, 2018

Note: there’s that other cargo cult of premature optimisation going around. Yes, you shouldn’t worry about these details too early in your application implementation. But you should still know when to worry about them, and be aware of the tradeoffs.
And while you’re still debating what name to give to that extracted method, I’ve written 5 new labeled if statements! 😉

X ITM Cloud News


Next Post

How to Unit Test Your Annotation Processor using jOOR

Sun Nov 24 , 2019
Spread the love          Annotation processors can be useful as a hacky workaround to get some language feature into the Java language. jOOQ also has an annotation processor that helps validate SQL syntax for: Plain SQL usage (SQL injection risk) SQL dialect support (prevent using an Oracle only feature on MySQL) You […]

Cloud Computing – Consultancy – Development – Hosting – APIs – Legacy Systems

X-ITM Technology helps our customers across the entire enterprise technology stack with differentiated industry solutions. We modernize IT, optimize data architectures, and make everything secure, scalable and orchestrated across public, private and hybrid clouds.

This image has an empty alt attribute; its file name is x-itmdc.jpg

The enterprise technology stack includes ITO; Cloud and Security Services; Applications and Industry IP; Data, Analytics and Engineering Services; and Advisory.

Watch an animation of  X-ITM‘s Enterprise Technology Stack

We combine years of experience running mission-critical systems with the latest digital innovations to deliver better business outcomes and new levels of performance, competitiveness and experiences for our customers and their stakeholders.

X-ITM invests in three key drivers of growth: People, Customers and Operational Execution.

The company’s global scale, talent and innovation platforms serve 6,000 private and public-sector clients in 70 countries.

X-ITM’s extensive partner network helps drive collaboration and leverage technology independence. The company has established more than 200 industry-leading global Partner Network relationships, including 15 strategic partners: Amazon Web Services, AT&T, Dell Technologies, Google Cloud, HCL, HP, HPE, IBM, Micro Focus, Microsoft, Oracle, PwC, SAP, ServiceNow and VMware