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.
Code is read more often than it is writtenBut 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:
woot:
if (something) {
for (Object o : list)
if (something(o))
break woot;
throw new E();
}
if (something) {
for (Object o : list)
if (something(o))
goto woot;
throw new E();
}
woot:
if (something && list.stream().noneMatch(this::something))
throw new E();
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;
}
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 internalStringBuilder
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
- Stream
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 {
@State(Scope.Benchmark)
public static class BenchmarkState {
boolean something = true;
@Param({ "2", "8" })
int listSize;
List<Integer> list = new ArrayList<>();
boolean something() {
return something;
}
boolean something(Integer o) {
return o > 2;
}
@Setup(Level.Trial)
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() && state.list.stream().noneMatch(state::something))
return 1;
return 0;
}
@Benchmark
public Object testStreamAnyMatch(BenchmarkState state) {
if (state.something() && !state.list.stream().anyMatch(state::something))
return 1;
return 0;
}
@Benchmark
public Object testStreamAllMatch(BenchmarkState state) {
if (state.something() && state.list.stream().allMatch(s -> !state.something(s)))
return 1;
return 0;
}
}
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/sWith 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:
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! ;-)
Do you really use !Boolean.FALSE.equals(someBooleanValue)? While I love concise syntax, I wouldn’t do it…. at least not more than once, namely in a method called isTrueOrNull or alike. The double negation makes it hard to read, especially when more such things come together.
Yes of course, why not? Of course, I static-import TRUE and FALSE for even more conciseness. Yes, I’d like something like notEquals() on Object, but hey…