Performance drag of Java assertions when disabled Performance drag of Java assertions when disabled java java

Performance drag of Java assertions when disabled


Contrary to the conventional wisdom, asserts do have a runtime impact and may affect performance. This impact is likely to be small in most cases but could be large in certain circumstances. Some of the mechanisms by which asserts slow things down at runtime are fairly "smooth" and predictable (and generally small), but the last way discussed below (failure to inline) is tricky because it is the largest potential issue (you could have an order-of-magnitude regression) and it isn't smooth1.

Analysis

Assert Implementation

When it comes to analyzing the assert functionality in Java, a nice thing is that they aren't anything magic at the bytecode/JVM level. That is, they are implemented in the .class file using standard Java mechanics at (.java file) compile time, and they don't get any special treatment by the JVM2, but rely on the usual optimizations that apply to any runtime compiled code.

Let's take a quick look at exactly how they are implemented on a modern Oracle 8 JDK (but AFAIK it hasn't changed in pretty much forever).

Take the following method with a single assert:

public int addAssert(int x, int y) {    assert x > 0 && y > 0;    return x + y;} 

... compile that method and decompile the bytecode with javap -c foo.bar.Main:

  public int addAssert(int, int);    Code:       0: getstatic     #17                 // Field $assertionsDisabled:Z       3: ifne          22       6: iload_1       7: ifle          14      10: iload_2      11: ifgt          22      14: new           #39                 // class java/lang/AssertionError      17: dup      18: invokespecial #41                 // Method java/lang/AssertionError."<init>":()V      21: athrow      22: iload_1      23: iload_2      24: iadd      25: ireturn

The first 22 bytes of bytecode are all associated with the assert. Right up front, it checks the hidden static $assertionsDisabled field and jumps over all the assert logic if it is true. Otherwise, it just does the two checks in the usual way, and constructs and throws an AssertionError() object if they fail.

So there is nothing really special about assert support at the bytecode level - the only trick is the $assertionsDisabled field, which - using the same javap output - we can see is a static final initialized at class init time:

  static final boolean $assertionsDisabled;  static {};    Code:       0: ldc           #1                  // class foo/Scrap       2: invokevirtual #11                 // Method java/lang/Class.desiredAssertionStatus:()Z       5: ifne          12       8: iconst_1       9: goto          13      12: iconst_0      13: putstatic     #17                 // Field $assertionsDisabled:Z

So the compiler has created this hidden static final field and loads it based on the public desiredAssertionStatus() method.

So nothing magic at all. In fact, let's try to do the same thing ourselves, with our own static SKIP_CHECKS field that we load based on a system property:

public static final boolean SKIP_CHECKS = Boolean.getBoolean("skip.checks");public int addHomebrew(int x, int y) {    if (!SKIP_CHECKS) {        if (!(x > 0 && y > 0)) {            throw new AssertionError();        }    }    return x + y;}

Here we just write out longhand what the assertion is doing (we could even combine the if statements, but we'll try to match the assert as closely as possible). Let's check the output:

 public int addHomebrew(int, int);    Code:       0: getstatic     #18                 // Field SKIP_CHECKS:Z       3: ifne          22       6: iload_1       7: ifle          14      10: iload_2      11: ifgt          22      14: new           #33                 // class java/lang/AssertionError      17: dup      18: invokespecial #35                 // Method java/lang/AssertionError."<init>":()V      21: athrow      22: iload_1      23: iload_2      24: iadd      25: ireturn

Huh, it's pretty much bytecode-for-bytecode identical to the assert version.

Assert Costs

So we can pretty much reduce the "how expensive is an assert" question to "how expensive is a code jumped over by an always-taken branch based on a static final condition?". The good news then is that such branches are generally completely optimized away by the C2 compiler, if the method is compiled. Of course, even in that case, you still pay some costs:

  1. The class files are larger, and there is more code to JIT.
  2. Prior to JIT, the interpreted version will likely run slower.
  3. The full size of the function is used in inlining decisions, and so the presence of asserts affects this decision even when disabled.

Points (1) and (2) are a direct consequence of the assert being removed during runtime compile (JIT), rather than at java-file-compile time. This is a key difference with C and C++ asserts (but in exchange you get to decide to use asserts on each launch of the binary, rather than compiling in that decision).

Point (3) is probably the most critical, and is rarely mentioned and is hard to analyze. The basic idea is that the JIT uses a couple size thresholds when making inlining decisions - one small threshold (~30 bytes) under which it almost always inlines, and another larger threshold (~300 bytes) over which it never inlines. Between the thresholds, whether it inlines or not depends on whether the method is hot or not, and other heuristics such as whether it has already been inlined elsewhere.

Since the thresholds are based on the bytecode size, the use of asserts can dramatically affect those decisions - in the example above, fully 22 of the 26 bytes in the function were assert related. Especially when using many small methods, it is easy for asserts to push a method over the inlining thresholds. Now the thresholds are just heuristics, so it's possible that changing a method from inline to not-inline could improve performance in some cases - but in general you want more rather than less inlining since it is a grand-daddy optimization that allows many more once it occurs.

Mitigation

One approach to work around this issue is to move most of the assert logic to a special function, as follows:

public int addAssertOutOfLine(int x, int y) {    assertInRange(x,y);    return x + y;}private static void assertInRange(int x, int y) {    assert x > 0 && y > 0;}

This compiles to:

  public int addAssertOutOfLine(int, int);    Code:       0: iload_1       1: iload_2       2: invokestatic  #46                 // Method assertInRange:(II)V       5: iload_1       6: iload_2       7: iadd       8: ireturn

... and so has reduced the size of that function from 26 to 9 bytes, of which 5 are assert related. Of course, the missing bytecode has just moved to the other function, but that's fine because it will be considered separately in inlining decisions and JIT-compiles to a no-op when asserts are disabled.

True Compile-Time Asserts

Finally, it's worth noting that you can get C/C++-like compile-time asserts if you want. These are asserts whose on/off status is statically compiled into the binary (at javac time). If you want to enable asserts, you need a new binary. On the other hand, this type of assert is truly free at runtime.

If we change the homebrew SKIP_CHECKS static final to be known at compile time, like so:

public static final boolean SKIP_CHECKS = true;

then addHomebrew compiles down to:

  public int addHomebrew(int, int);Code:   0: iload_1   1: iload_2   2: iadd   3: ireturn

That is, there is no trace left of the assert. In this case we can truly say there is zero runtime cost. You could make this more workable across a project by having a single StaticAssert class that wraps the SKIP_CHECKS variable, and you can leverage this existing assert sugar to make a 1-line version:

public int addHomebrew2(int x, int y) {    assert SKIP_CHECKS || (x > 0 && y > 0);    return x + y;}

Again, this compiles at javac time down to bytecode without a trace of the assert. You are going to have to deal with an IDE warning about dead code though (at least in eclipse).


1 By this, I mean that this issue may have zero effect, and then after a small innocuous change to surrounding code it may suddenly have a big effect. Basically the various penalty levels are heavily quantized due to the binary effect of the "to inline or not to inline" decisions.

2 At least for the all-important part of compiling/running the assert-related code at runtime. Of course there is a small amount of support in the JVM for accepting the -ea command line argument and flipping the default assertion status (but as above you can accomplish the same effect in a generic way with properties).


Very very little. I believe they are removed during class loading.

The closest thing I've got to some proof is: The assert statement specification in the Java Langauge Specification. It seems to be worded so that the assert statements can be processed at class load time.


Disabling assertions eliminates their performance penalty entirely. Once disabled, they are essentially equivalent to empty statements in semantics and performance

Source