Monday, June 18, 2012

Real world bytecode engineering (BCE)


I'm a big fan of bytecode engineering. You can do wonderful things with it if you just know how. Here's a case, episode one of the series to come. There may be other ways to solve this problem, but since I didn't find any way to catch floating point exceptions in Java, I decided to implement it with BCE.

The problem


A group of people is developing mathematical program which is quite complex and large. The calculations are done, due to performance reasons, with normal IEEE 754 double precision numbers (i.e. doubles). In addition, operations of java.lang.Math are not available without using primitive types. With java.math.BigDecimal these floating point exceptions (FPEs) would actually blow out the application immediately, but that’s not the case with primitive types. Consider the following fragment:


public class BigDecimalArithmetic {

   public static void main( String... args ) {

      double a = 5.0, b = 0.0, c = 10.0;

      System.out.println( c / ( a / b + 5 ) );

   }

}



Magically this program prints out 0.0 without any exceptions. Why? Since 5.0 / 0.0 results in infinity, infinity plus 5 is still infinity and finally 5.0 divided by infinity is 0.0. I would say this is quite often not what you want. Now consider 100 000 lines of code full of this kind of logic. Houston, we have a problem.


How to find problematic places?


To catch the problem we can test if the calculation resulted in infinity. That's what we do:



public class BigDecimalArithmetic {
   public static void main( String... args ) {
      double a = 5.0, b = 0.0, c = 10.0;
      double r1 = a / b;
      if( Double.isInfinite( r1 ) )
         throw new ArithmeticException( "division by zero" );
      System.out.println( c / ( r1 + 5 ) );
   }
}



Now we know if the calculation results in infinity and if so, we throw an ArithmeticException. However, we still don’t catch the situation when calculation resulted in a NaN. NaN can be produced, for example by taking square root of -1. To catch a NaN situation, once again we modify our program. This is what we have now:



public class BigDecimalArithmetic {
   public static void main( String... args ) {
      double a = 5.0, b = 0.0, c = 10.0;
      double r1 = a / b;
      if( Double.isInfinite( r1 ) )
         throw new ArithmeticException( "division by zero" );
      if( Double.isNan( r1 ) )
         throw new ArithmeticException( "result is NaN" );
      System.out.println( c / ( r1 + 5 ) );
   }
}


We have completed one division operation and we sure we are still doing OK with the results. Now consider executing some real world mathematics. The number of operations is huge and to know exactly where you went wrong means excessive if-blocks; the code would be bloated with exception handling logic. And afterall, we have to modify our code base to catch those special situations.


Finding problems without touching the code?



This really sounds good, since the number of lines of code is simply too much to manually go thru. How do we modify behavior without touching the code? The simple answer is bytecode engineering. We modify bytecode of classes at runtime just after the classes are loaded but before the bytecode is verified. Once the class is loaded, and the bytecode is changed on the fly, it will behave just like those checks (isNan and isInfinite) were ubiquitously present. Bytecode engineering (BCE) is not business as usual for many Java developers and without proper tools it's really hard. My favorite library for BCE is ASM from ObjectWeb. I'm also using Andrey Loskutov's Bytecode Outline. It runs as Eclipse plugin. I think it's a must-have tool for ASM developers. Remember, I never said BCE is easy, but with a little assembler or C background you can probably manage with it. See ObjectWeb for more information. But finally we dive into BCE. Fasten your seatbelts!


Diving into wonderful world of bytecode engineering



Now we know what we should do after each FP operation and we have selected our BCE library. So lets start producing some bytecode with ASM. We start by introducing a private method which will generate the bytecode for the checks. Also, keep in mind that this method is called only during the class loading time and not after that. The method  looks like this:


private void generateChecks( String operation ) {
   dup2( );
   // Is it infinite?
   mv.visitMethodInsn( INVOKESTATIC,"java/lang/Double","isInfinite","(D)Z" );
   Label label1 = new Label( );
   mv.visitJumpInsn( IFEQ,label1 );
   mv.visitLdcInsn( operation );
   mv.visitMethodInsn( INVOKESTATIC,"paci/samples/bce/FPEListener",
      "resultIsInfinite","(Ljava/lang/String;)V" );
   mv.visitLabel( label1 );

   dup2( );
   // Is it NaN?
   mv.visitMethodInsn( INVOKESTATIC,"java/lang/Double","isNan","(D)Z" );
   Label label2 = new Label( );
   mv.visitJumpInsn( IFEQ,label2 );
   mv.visitLdcInsn( operation );
   mv.visitMethodInsn( INVOKESTATIC,"paci/samples/bce/FPEListener",
      "resultIsInfinite","(Ljava/lang/String;)V" );
   mv.visitLabel( label2 );
}


Upon class loading process ASM will call visitInsn method for each and every instruction in the classfile. In my overridden version of visitInsn I have a simple switch-case statement which will generate some extra bytecode for selected operations. My visitInsn method now looks like this:


@Override
public void visitInsn( int opcode ) {
   mv.visitInsn( opcode );
   switch( opcode ) {
      case DDIV :
         generateChecks( "DIVIDE" );
         break;
      case DMUL :
         generateChecks( "MULTIPLY" );
         break;
      case DADD :
         generateChecks( "ADD" );
         break;
      case DSUB :
         generateChecks( "SUBTRACT" );
         break;
   }
}


First I let the ASM to generate the operation itself (e.g ddiv, dmul, ...). If the operation is dealing with double precision arithmetic, I will call my already well understood generateChecks method. And remember, this only happens during classloading time. This method will generate the checks right after each operation. Now, if a class dividing two doubles is instrumented with our bytecode modifier, at runtime the bytecode will look like this:


21:  ddiv
22:  dup2
23:  invokestatic    #22; //Method java/lang/Double. isInfinite:(D)Z
26:  ifeq    34
29:  ldc     #24; //String DIVIDE
31:  invokestatic    #30; //Method paci/samples/bce/FPEListener.resultIsInfinite:(Ljava/lang/String;)V
34:  dup2
35:  invokestatic    #33; //Method java/lang/Double.isNaN:(D)Z
38:  ifeq    46
41:  ldc     #24; //String DIVIDE
43:  invokestatic    #36; //Method paci/samples/bce/FPEListener.resultIsNaN:(Ljava/lang/String;)V



Line 21 performs actual division operation (ddiv) which pushes the result on the top of the stack. This is how JVM operation generally work. Then we duplicate top two stack words because we cannot consume the result of the computation with our java.lang.Double.isNaN call at line 23. If the result of isInfinite is zero (false) we jump to line 34 and duplicate the top two stack words again for the same reason as before. Next lines perform the similar check for NaN. If computation didn't cause any floating point exception, the top of the stack still contains the result of double division. Note that bytecode produced by my bytecode modifier is not strictly representable in Java! This is my best guess for the same behavior:


private static void test( ) {
   double a = 5, b = 6;
   double result = a / b;
   if( Double.isInfinite( result ) )
      FPEListener.resultIsInfinite"DIVIDE" );
   if( Double.isNaN( result ) )
   FPEListener.resultIsNan"DIVIDE" );
}




But if we look at the bytecode generated by the compiler, we can see it's not using dup2 as I do with my ASM code. Instead, it has replaced the dup2's with local variable store and load operations (dstore and dload). Here's the bytecode for method test:


private static void test();
  Code:
   0:   ldc2_w  #15; //double 5.0d
   3:   dstore_0
   4:   ldc2_w  #17; //double 6.0d
   7:   dstore_2
   8:   dload_0
   9:   dload_2
   10:  ddiv
   11:  dstore  4
   13:  dload   4
   15:  invokestatic    #19; //Method java/lang/Double.isInfinite:(D)Z
   18:  ifeq    26
   21:  ldc     #25; //String DIVIDE
   23:  invokestatic    #27; //Method paci/samples/bce/FPEListener.resultIsInfinite:(Ljava/lang/String;)V
   26:  dload   4
   28:  invokestatic    #33; //Method java/lang/Double.isNaN:(D)Z
   31:  ifeq    39
   34:  ldc     #25; //String DIVIDE
   36:  invokestatic    #36; //Method paci/samples/bce/FPEListener.resultIsNan:(Ljava/lang/String;)V
   39:  return

Since mathematicians also use some methods of java.lang.Math, I also decided to add support for every method in java.lang.Math which takes a single double as a parameter and returns a double. This was so easy to do with ASM since it provides a hook to handle all method calls. My overridden method looks like this:



@Override
public void visitMethodInsn( int opcode,String owner,String name,String desc ) {
   super.visitMethodInsn( opcode,owner,name,desc );
   // Intercept all java.lang.Math calls which are static, take one
   // double as an argument and return double
   if( opcode == INVOKESTATIC && owner.equals( "java/lang/Math" ) && 
       desc.equals( "(D)D" ) ) {
      generateChecks(  name.toUpperCase( ) );
   }
}

Verification


After some mysterious coding session we can now verify the results. If we run for example thes lines:



double a = 50.0, b = 10.0, c = 0.0;
double result = a / b / c * 4;



it will call FPEListener which will in turn throw an java.lang.ArithmeticException. The stack trace looks like this:



Exception in thread "main" java.lang.ArithmeticException: Floating point operation DIVIDE resulted infinite
           at paci.samples.bce.FPEListener.resultIsInfinite(FPEListener.java:9)
           at paci.Liukuuliukuu.main(Liukuuliukuu.java:30)


The reason I did it this way (instead of just throwing ArithmeticException) is that now it's easier to change the error handling behavior, e.g. whether it throws and exception or just prints out the stack trace.

That's it. The full code can be found here. I used ASM 3.1 with it, and it may have some problems with ASM 4.0.

The next story about BCE will be dealing with selectively adjusting time seen by the applications.

No comments:

Post a Comment