Over a million developers have joined DZone.

Does javac do optimization? Doesn't seem like it ...

· Performance Zone

See Gartner’s latest research on the application performance monitoring landscape and how APM suites are becoming more and more critical to the business, brought to you in partnership with AppDynamics.

We usually say that Java programmers have to write code that looks good and all other issues are solved by the compiler. For example, having a complex Boolean expression is better moved to a separate method with a good name and with a single return statement containing the expression. The original if or while will be much easier to understand. The Java compiler is clever enough to see that the code is only called from a single place and will move the code inline.

Is that really true? I have heard that the JIT compiler does the optimization and the javac compiler does not. Let's have a look at a simple class:

public class OptimizeThis {
    private int a(int x, int y) {
        return x + y;
    }
 
    public int add(int x, int y, int z) {
        return a(a(x, y), z);
    }
}

There is a lot of space for optimization. The method a() could be left out from all the fun. The code could be included in the method add() and the code would be much faster.
Something like this:

public class Optimized {
    public int add(int x, int y, int z) {
        return x + y + z;
    }
}

Let's compile the class OptimizeThis and disassemble using javap:

verhasp:java verhasp$ javac OptimizeThis.java
$ javap -v -p OptimizeThis.class
Classfile /Users/verhasp/.../src/main/java/OptimizeThis.class
  Last modified 2012.07.08.; size 327 bytes
  MD5 checksum 9ba33fe0979ff0948a683fab2dc32d12
  Compiled from "OptimizeThis.java"
public class OptimizeThis
  SourceFile: "OptimizeThis.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Methodref          #3.#16         //  OptimizeThis.a:(II)I
   #3 = Class              #17            //  OptimizeThis
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               a
  #10 = Utf8               (II)I
  #11 = Utf8               add
  #12 = Utf8               (III)I
  #13 = Utf8               SourceFile
  #14 = Utf8               OptimizeThis.java
  #15 = NameAndType        #5:#6          //  "<init>":()V
  #16 = NameAndType        #9:#10         //  a:(II)I
  #17 = Utf8               OptimizeThis
  #18 = Utf8               java/lang/Object
{
  public OptimizeThis();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
 
  private int a(int, int);
    flags: ACC_PRIVATE
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 3: 0
 
  public int add(int, int, int);
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=4, args_size=4
         0: aload_0
         1: aload_0
         2: iload_1
         3: iload_2
         4: invokespecial #2                  // Method a:(II)I
         7: iload_3
         8: invokespecial #2                  // Method a:(II)I
        11: ireturn
      LineNumberTable:
        line 7: 0
}
verhasp:java verhasp$

You can see we have both of the methods. The code

private int a(int, int);
  flags: ACC_PRIVATE
  Code:
    stack=2, locals=3, args_size=3
       0: iload_1
       1: iload_2
       2: iadd
       3: ireturn

is the private method a() and the code

public int add(int, int, int);
  flags: ACC_PUBLIC
  Code:
    stack=4, locals=4, args_size=4
       0: aload_0
       1: aload_0
       2: iload_1
       3: iload_2
       4: invokespecial #2                  // Method a:(II)I
       7: iload_3
       8: invokespecial #2                  // Method a:(II)I
      11: ireturn

is the public method add(). The code itself is simple. The method a() loads on the operand stack the first local variable (iload_1), then it does the same with the second (iload_2), and then adds the two (iadd). What is left on the operand stack is used to return (ireturn).

  1. The local variable number zero is this in case of non-static methods.
  2. The arguments are also treated as local variables.
  3. For the first few local variables, there are shorthand Java byte codes because the generated code accesses these the most and this saves some space and speed.
  4. We are using int only and thus we need not care about more complex issues like a double occupying two slots.

The method add() is almost as simple. It loads the value of this on the operand stack two times. It is needed to call the non-static method a(). After that, it loads the first and the second local variable on the stack (the first two method arguments) and in command number 4 (line 61) calls the method a(). After this, it loads the third local variable on the stack. This time the stack contains the variable this, the result of the previous call to method a() and then the third local variable, which is the third argument to the method add(). Then it calls the method a().

Let's have a look at the code generated from the class Optimized:

$ javap -v -p Optimized.class
Classfile /Users/verhasp/.../src/main/java/Optimized.class
  Last modified 2012.07.08.; size 251 bytes
  MD5 checksum 2765acd1d55048184e9632c1a14a8e21
  Compiled from "Optimized.java"
public class Optimized
  SourceFile: "Optimized.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         //  java/lang/Object."<init>":()V
   #2 = Class              #13            //  Optimized
   #3 = Class              #14            //  java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               add
   #9 = Utf8               (III)I
  #10 = Utf8               SourceFile
  #11 = Utf8               Optimized.java
  #12 = NameAndType        #4:#5          //  "<init>":()V
  #13 = Utf8               Optimized
  #14 = Utf8               java/lang/Object
{
  public Optimized();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
 
  public int add(int, int, int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=4
         0: iload_1
         1: iload_2
         2: iadd
         3: iload_3
         4: iadd
         5: ireturn
      LineNumberTable:
        line 3: 0
}

Much simpler. Is it also faster? The proof of the pudding is in the eating. If it is not tasty then the dog will eat it. However …

Here we have again the two classes extended with some main methods (one for each):

public class OptimizeThis {
    private int a(int x, int y) {
        return x + y;
    }
 
    public int add(int x, int y, int z) {
        return a(a(x, y), z);
    }
 
    public static void main(String[] args) {
        OptimizeThis adder = new OptimizeThis();
        final int outer = 100_0000_000;
        final int loop = 100_0000_000;
        Long tStart = System.currentTimeMillis();
        for (int j = 0; j < outer; j++) {
            for (int i = 0; i < loop; i++) {
                int x = 1;
                int y = 2;
                int z = 3;
                adder.add(x, y, z);
            }
        }
        Long tEnd = System.currentTimeMillis();
        System.out.println(tEnd - tStart);
    }
}

and

public class Optimized {
    public int add(int x, int y, int z) {
        return x + y + z;
    }
 
    public static void main(String[] args) {
        Optimized adder = new Optimized();
        final int outer = 100_0000_000;
        final int loop = 100_0000_000;
        Long tStart = System.currentTimeMillis();
        for (int j = 0; j < outer; j++) {
            for (int i = 0; i < loop; i++) {
                int x = 1;
                int y = 2;
                int z = 3;
                adder.add(x, y, z);
            }
        }
        Long tEnd = System.currentTimeMillis();
        System.out.println(tEnd - tStart);
    }
}

In addition to this we also created an empty class, named Empty that returns the constant zero.

public class Empty {
    public int add(int x, int y, int z) {
        return 0;
    }
 
    public static void main(String[] args) {
        Empty adder = new Empty();
        final int outer = 100_0000_000;
        final int loop = 100_0000_000;
        Long tStart = System.currentTimeMillis();
        for (int j = 0; j < outer; j++) {
            for (int i = 0; i < loop; i++) {
                int x = 1;
                int y = 2;
                int z = 3;
                adder.add(x, y, z);
            }
        }
        Long tEnd = System.currentTimeMillis();
        System.out.println(tEnd - tStart);
    }
}

Here we have an executing script that can be called after executing the command javac *.java:

#! /bin/sh
echo "Empty"
java Empty
echo "Optimized"
java Optimized
echo "OptimizeThis"
java OptimizeThis

And the result:
STOP! Before you open it, try to estimate the rationale between the optimized and non-optimized version and also how much faster the class Empty is. If you have your estimation, you can open the result:

verhasp:java verhasp$ ./testrun.sh
Empty
1970
Optimized
1987
OptimizeThis
1970
verhasp:java verhasp$ ./testrun.sh
Empty
1986
Optimized
2026
OptimizeThis
2001
verhasp:java verhasp$ ./testrun.sh
Empty
1917
Optimized
1892
OptimizeThis
1899
verhasp:java verhasp$ ./testrun.sh
Empty
1908
Optimized
1903
OptimizeThis
1899
verhasp:java verhasp$ ./testrun.sh
Empty
1898
Optimized
1891
OptimizeThis
1892
verhasp:java verhasp$ ./testrun.sh
Empty
1896
Optimized
1896
OptimizeThis
1897
verhasp:java verhasp$ ./testrun.sh
Empty
1897
Optimized
1903
OptimizeThis
1897
verhasp:java verhasp$ ./testrun.sh
Empty
1908
Optimized
1892
OptimizeThis
1900
verhasp:java verhasp$ ./testrun.sh
Empty
1899
Optimized
1905
OptimizeThis
1904
verhasp:java verhasp$ ./testrun.sh
Empty
1891
Optimized
1896
OptimizeThis
1896
verhasp:java verhasp$ ./testrun.sh
Empty
1895
Optimized
1891
OptimizeThis
1904
verhasp:java verhasp$ ./testrun.sh
Empty
1898
Optimized
1889
OptimizeThis
1894
verhasp:java verhasp$ ./testrun.sh
Empty
1917
Optimized
1894
OptimizeThis
1898
verhasp:java verhasp$

Conclusion? Before you vote on the first choice, read all the possible answers!

Click here to view the poll.

The tests were executed on a 8GB memory MacBook Pro7.1 with OS X 10.7.4, 7-es Java (you could notice it that it was already Java 7). Here is the output of java -version:

verhasp:java verhasp$ java -version
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b21)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)

The Performance Zone is brought to you in partnership with AppDynamics.  See Gartner’s latest research on the application performance monitoring landscape and how APM suites are becoming more and more critical to the business.

Topics:

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}