6.2 Assertions

SDK 1.4 added assertions to the Java specification. Assertions allow you to add statements into your code of the form:

assert boolean_expression;

where boolean_expression is any valid Java expression that evaluates to produce a boolean result. If the boolean expression evaluates to false, an AssertionError is thrown. A second form of the assert statement allows an additional parameter that evaluates to a String:

assert boolean_expression : String_expression;

The second form allows customization of the error message produced when an assertion fails. For example:

assert param1 >= 0 : 
 "Parameter param1 must be non-negative, but was " + param1;

The difference between an assert statement and a normal statement is that the assert statement can be disabled at runtime. More precisely, assertions can be enabled or disabled at runtime, and you can specify separately for each class whether its assertions are enabled or disabled using the -ea and -da parameters of the java command. Assertions can also be enabled and disabled from code using the various Classloader.set*AssertionStatus( ) methods. The assertion status of a class is fixed once that class has been loaded. The classloader enables or disables assert statements.

The only limitation of the assert keyword is that it must be used in an executable block. An assertion cannot be used with class variable declarations, for instance, but can be placed within any method.

Using assertions generally improves the quality of code and assists in diagnosing problems in an application. Application code should be written so that it is functionally the same when running with assertions disabled. Assertions that cannot be disabled without altering the functionality of the code should not be defined as assertions.

The assert keyword is not recognized by compilers prior to 1.4. To use assertions with 1.4, you need to specify to the compiler that the Java source file is from SDK 1.4, using the parameters -source 1.4. Unfortunately, the resulting bytecode can not be run under pre-1.4 JVMs.

6.2.1 Assertion Overhead

It is useful to understand how the assertion mechanism works to see how assertion statements can affect performance. When the compiler finds an assertion in a class, it adds a generated static final field named $assertionsDisabled to the class. The field is left unassigned (this is legal bytecode). The assertion itself is compiled into a statement of the form:

if ($assertionsDisabled)
  if (!boolean_expression)
    throw new AssertionError(String_expression);

At classloading time, the classloader determines the assertion status of the class using the various rules listed in the assertion specification, and sets the $assertionsDisabled field accordingly. This means that without any further optimizations, every assert imposes a minimum of one runtime boolean test, even if assertions have been disabled.

There are two potential optimizations that could completely eliminate this assertion overhead. First, the classloader itself could strip out the assertion status test and the subsequent if statement when assertions are disabled. As the $assertionsDisabled is a final variable, there is no prospect of the value changing later, so the test is not actually necessary: when assertions are enabled the boolean expression if statement would be executed directly with no assertion status test; when assertions are disabled the boolean expression if statement would be completely eliminated together with the assertion status test. In practice, however, Sun has not enabled the classloader to strip the assertion status test.

As a result, we must consider an alternative optimization. This one uses exactly the same procedure outlined for the classloader optimization but, instead of the classloader, the JIT compiler would strip out the unnecessary statements. In fact, this simply applies the standard compiler optimization of eliminating dead code. This is the approach taken by Sun, which has left the optimization to the HotSpot JIT compiler. This means that, at least for the initial 1.4 release, the overhead of assert statements is dependent on whether a JVM strips those statements. For example, the 1.4 JVM running in client (default) mode is not sufficiently aggressive in JIT compilation optimizations to eliminate assertion overhead. However, when running the same JVM in server mode (with the -server parameter), the JIT compiler effectively eliminates disabled assertion statements.

Table 6-3 and Table 6-4 list the results of testing code with assertion statements compared to using an explicit and equivalent if...throw test and to the same code with no tests. As the overhead of the assertion depends on what it is being compared against, I used two separate baselines, comparing assertion cost against a very quick test, essentially just a return statement, and against a more complex, slower test. The test class code is listed shortly after the results.

Table 6-3. Overhead from an assertion statement in a very simple method

Simple method test

java -client

java -server

no check or assert

100%

0%[5]

with assert disabled

235%

57%

with assert enabled

243%

194%

with explicit check

137%

192%

[5] The server mode JIT compiler inlined the quick test into the test loop, resulting in no method call overhead at all and a legible measurement for test time. Interestingly, the server mode fails to do this when the assert is disabled.

Table 6-4. Overhead from an assertion statement in a complex method

Complex method test

java -client

java -server

no check or assert

100%

95%

with assert disabled

100%

95%

with assert enabled

107%

95%

with explicit check

109%

95%

Clearly, assertions can add significant overhead to short methods. You may want to avoid adding assertions willy-nilly to setters, getters, and other short, frequently called methods. However, there is no need to become paranoid about whether or not to add assertions. It is probably better initially to add assertions as desired irrespective of performance considerations, then catch and eliminate any expensive assertions using a profiler. For longer methods, assertions can be added without too much concern.

But do note that when enabled, any assertion takes at least as long to run as its boolean_expression evaluation takes. Consequently, code running with assertions enabled will definitely be slower than code running with assertions disabled, even if only a few percent slower. If possible, you should run the application with as many assertions disabled as possible. Similarly, since assertions can be turned off but explicit checks cannot, you should consider changing all explicit checks for incorrect parameters and state in your code to use assertions instead of explicitly using if...throw statements. For example, IllegalArgumentExceptions often test for documented incorrect conditions, and these tests could be changed to assertions. The decision about whether any particular test can be changed to an assertion ultimately comes down to whether the test should always be present (don't make it an assertion), or whether the test is optional and provides extra robustness, especially during development and testing (definitely an assertion candidate).

Finally, remember to profile the application as it will be run in practice, with the same mixture of assertions turned on or off. Don't make the mistake of profiling the application with all assertions turned off or turned on if that is not the way the application will be run when deployed.

package tuning.asserts;
  
public class AssertTest1 {
  
  static int sval;
  static int[  ] some_array = {3,5,9,16,5,18,23,66,28,19};
  
  public static int testWithNoCheck(int val)
  {
//alternative short test
//    return val+2;
    double x = Math.cos(val%Math.PI);
    double y = Math.sin(val%Math.PI);
    double d = x*x + y*y;
    d = Math.sqrt(d);
    return Math.abs(val - some_array[((int) Math.abs(d))%10]);
  }
  
  public static int testWithExplicitCheck(int val)
  {
    if (val < 0)
      throw new IllegalArgumentException("parameter val should be positive, but is " 
+ val);
//alternative short test
//    return val+2;
    double x = Math.cos(val%Math.PI);
    double y = Math.sin(val%Math.PI);
    double d = x*x + y*y;
    d = Math.sqrt(d);
    return Math.abs(val - some_array[((int) Math.abs(d))%10]);
  }
  
  public static int testWithAssert(int val)
  {
    assert (val >= 0) : "parameter val should be positive, but is " + val;
//alternative short test
//    return val+2;
    double x = Math.cos(val%Math.PI);
    double y = Math.sin(val%Math.PI);
    double d = x*x + y*y;
    d = Math.sqrt(d);
    return Math.abs(val - some_array[((int) Math.abs(d))%10]);
  }
  
  public static void main(String[  ] args)
  {
    test(args);
    test(args);
  }
  public static void test(String[  ] args)
  {
    try
    {
      testWithAssert(-1);
      System.out.println("Asserts off");
    }
    catch(Throwable t)
    {
      System.out.println("Asserts on");
    }
  
    int REPEAT = Integer.parseInt(args[0]);
  
    int v = 0;
    long time = System.currentTimeMillis(  );
    sval = 0;
    for (int i = 0; i < REPEAT/10; i++)
      v = testWithNoCheck(v);
  
    v = 0;
    sval = 0;
    time = System.currentTimeMillis(  );
    for (int i = 0; i < REPEAT; i++)
      v = testWithExplicitCheck(v);
    time = System.currentTimeMillis(  ) - time;
    System.out.println("testWithExplicitCheck took " + time + " millis, val = " + v + 
" sval = " + sval);
  
    v = 0;
    sval = 0;
    time = System.currentTimeMillis(  );
    for (int i = 0; i < REPEAT; i++)
      v = testWithAssert(v);
    time = System.currentTimeMillis(  ) - time;
    System.out.println("testWithAssert took " + time + " millis, val = " + v + " sval 
= " + sval);
  
    v = 0;
    sval = 0;
    time = System.currentTimeMillis(  );
    for (int i = 0; i < REPEAT; i++)
      v = testWithNoCheck(v);
    time = System.currentTimeMillis(  ) - time;
    System.out.println("testWithNoCheck took " + time + " millis, val = " + v + " 
sval = " + sval);
  
  }
}