I'm working on a small scene graph implementation in Java 8. The basic scene node looks something like this:
public class SceneNode {
private final List<SceneNode> children = new ArrayList<>();
protected Runnable preRender;
protected Runnable postRender;
protected Runnable render;
public final void render() {
preRender.run();
render.run();
for (Renderable child : children) {
child.render();
}
postRender.run();
}
}
This works fine if the Runnables
default to () -> {}
. However, alternatively I could allow them to be null, but that means that render()
method has to look like this:
public final void render() {
if (null != preRender) { preRender.run(); }
if (null != render) { render.run(); }
for (Renderable child : children) {
child.render();
}
if (null != postRender) { postRender.run(); }
}
So my question is, is the implicit cost of the branching introduced by the null check likely to cost more or less than whatever the JVM ends up compiling an empty lambda into? It seems like it should end up costing more to check for null, because a potential branch limits optimization, while presumably the Java compiler or JVM should be smart enough to compile an empty lambda into a no-op.
Interestingly, it seems that checking for null is a little bit faster, than calling an empty lambda or an empty anonymous class, when the JVM is run with the -client
argument. When running with -server
, the performance is the same for all approaches.
I have done a micro benchmark with Caliper, to test this.
Here is the test class (latest Caliper form git necessary to compile):
@VmOptions("-client")
public class EmptyLambdaTest {
public Runnable emptyLambda = () -> {};
public Runnable emptyAnonymousType = new Runnable() {
@Override
public void run() {}
};
public Runnable nullAbleRunnable;
@Benchmark
public int timeEmptyLambda(int reps){
int dummy = 0;
for (int i = 0; i < reps; i++) {
emptyLambda.run();
dummy |= i;
}
return dummy;
}
@Benchmark
public int timeEmptyAnonymousType(int reps){
int dummy = 0;
for (int i = 0; i < reps; i++) {
emptyAnonymousType.run();
dummy |= i;
}
return dummy;
}
@Benchmark
public int timeNullCheck(int reps){
int dummy = 0;
for (int i = 0; i < reps; i++) {
if (nullAbleRunnable != null) {
nullAbleRunnable.run();
}
dummy |= i;
}
return dummy;
}
}
And here are the benchmark results:
Is defaulting to an empty lambda better or worse than checking for a potentially null lambda?
This is essentially the same as asking if it is better to test for a null String
parameter or try to substitute an empty String
.
The answer is that it depends on whether you want to treat the null
as a programming error ... or not.
My personal opinion is that unexpected null
s should be treated as programming errors, and that you should allow the program to crash with an NPE. That way, the problem will come to your attention earlier and will be easier to track down and fix ... than if you substituted some "make good" value to stop the NPE from being thrown.
But of course, that doesn't apply for expected null
values; i.e. when the API javadocs say that a null
is a permissible value, and say what it means.
This also relates to how you design your APIs. In this case, the issue is whether your API spec (i.e. the javadoc!) should insist on the programmer providing a no-op lambda, or treat null
as meaning the same thing. That boils down to a compromise between:
I'm more concerned about the implications of the runtime performance of using an empty lambda vs using a null and having to do a null check.
My intuition is that testing for null
would be faster, but any difference in performance will be small, and that the chances are that it won't be significant to the overall performance of the application.
(UPDATE - Turns out that my intuition is "half right" according to @Balder's micro-benchmarking. For a -client
mode JVM, null checking is a bit faster, but not enough to be concerning. For a -server
mode JVM, the JIT compiler is apparently optimizing both cases to native code with identical performance.)
I suggest that you treat that you would (or at least should) treat any potential optimization problem:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With