Code can be compiled with assertions in it and can be activated/deactivated when needed.
But if I deploy an app with assertions in it and those are disabled what is the penalty involved in therm being there and ignored?
The core reason it's not enabled by default is that assertions via assert are not meant to provide run-time validation/protection for production code. assert is a tool for use in development process and during testing that should not impact performance during actual running in production environment.
An assertion allows testing the correctness of any assumptions that have been made in the program. An assertion is achieved using the assert statement in Java. While executing assertion, it is believed to be true. If it fails, JVM throws an error named AssertionError.
Do not use assertions to check the parameters of a public method. An assert is inappropriate because the method guarantees that it will always enforce the argument checks. It must check its arguments whether or not assertions are enabled. Further, the assert construct does not throw an exception of the specified type.
Assertions should be used to check something that should never happen, while an exception should be used to check something that might happen. For example, a function might divide by 0, so an exception should be used, but an assertion could be used to check that the harddrive suddenly disappears.
Contrary to the conventional wisdom, asserts do have a runtime impact and may affect performance. The impact is likely to be small on average and zero in the median case, but it could be large when the stars align just right.
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.
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.
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:
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.
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.
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.
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