Running on Java 22-ea+27-2262 (Preview)
Home of The JavaSpecialists' Newsletter

105Performance Surprises in Tiger

Author: Dr. Heinz M. KabutzDate: 2005-03-28Java Version: 1.3Category: Performance
 

Abstract: Java 5 has some interesting performance optimizations than can make our code faster without our intervention. We look at some of the new features.

 

Welcome to the 105th edition of The Java(tm) Specialists' Newsletter, now also sent to Venezuela, our 109th country. It has been extremely busy this year, with company audits, and lots of travelling to far away countries. I am flying to Austria and Germany in April to present our Design Patterns Course, Java 5 (Tiger) Course, and last, but definitely not least, a section of the Java Performance Tuning Course authored by Kirk Pepperdine and Jack Shirazi. They are all presented in German, which is possible since that is my mother tongue, even though I grew up in South Africa. On my last visit, an official at the duty office remarked that he would never have guessed that I had been born outside of Germany :-)

Another trip is planned for May. One week training in Germany followed by a visit to Crete in Greece, where I plan to give a lecture on software development at the University of Crete in Iraklion, if all goes well. I went to Crete in December 2004, and it was easily the most hospitable place I have gone to.

Lots of travelling, and everywhere is far away when you live in the stunningly beautiful city of Cape Town.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Performance Surprises in Tiger

One thing that is certain about performance measurements in Java is that there is no certainty. Computers are by their nature deterministic, but you are still at the mercy of the compiler writers, the hotspot compilers, etc. With every version of Java, some parts are faster others slower. This becomes especially annoying when you have spent effort finetuning an application based on knowledge of what has always been true in Java, but suddenly changed without warning. Oh well, at least it will keep me writing newsletters ;-)

StringBuffer

For example, in the past, StringBuffer.toString() shared its char[] with the String object. If you changed the StringBuffer after calling toString(), it would make a copy of the char[] inside the StringBuffer, and work with that. Please have a look at my newsletter #68 where I discussed this phenomenon. Incidentally, Sun changed the setLength() method of JDK 1.4.1 back to what it was in JDK 1.4.0. I was discussing this behaviour with some Java programmers, and wanted to demonstrate that instead of calling setLength(0), you may as well just create a new StringBuffer, since the costly part of the creation is the char[].

Let's examine some code snippets:

    // SB1: Append, convert to String and release StringBuffer
    StringBuffer buf = new StringBuffer();
    buf.append(0);
    buf.append(1);
    buf.append(2);
    // etc.
    buf.toString();
    // SB2: Create with correct length, throw away afterwards
    StringBuffer buf = new StringBuffer(3231);
    buf.append(0);
    buf.append(1);
    buf.append(2);
    // etc.
    buf.toString();
    // SB3: buf defined somewhere else
    buf.setLength(0);
    buf.append(0);
    buf.append(1);
    buf.append(2);
    // etc.
    buf.toString(); // don't release buf

If we look at the table below, we see that for JDK 1.4.x, SB3 was approximately the same speed as SB1, and it was always slower than SB2. In JDK 1.5.0_02, suddenly SB2 and SB3 are approximately the same, with both being much faster than SB1.

Java Version Hotspot Type SB1 SB2 SB3
1.4.0_04 Client 1653 1452 1632
1.4.0_04 Server 1082 951 1072
1.4.1_07 Client 1752 1582 1723
1.4.1_07 Server 1101 962 1061
1.4.2_05 Client 1072 871 1071
1.4.2_05 Server 630 551 681
1.5.0_02 Client 1032 501 490
1.5.0_02 Server 811 400 381

Since JDK 1.5, when we do not need StringBuffer to be synchronized, we can replace it by the StringBuilder. Running the tests using the new class yields better results:

Java Version Hotspot Type SB1 SB2 SB3
1.5.0_02 Client 921 441 431
1.5.0_02 Server 721 350 341

For completeness, here is the code used to test the performance:

public class StringBufferTest {
  private static final int UPTO = 10 * 1000;
  private final int repeats;

  public StringBufferTest(int repeats) {
    this.repeats = repeats;
  }

  private long testNewBufferDefault() {
    long time = System.currentTimeMillis();
    for (int i = 0; i < repeats; i++) {
      StringBuffer buf = new StringBuffer();
      for (int j = 0; j < UPTO; j++) {
        buf.append(j);
      }
      buf.toString();
    }
    time = System.currentTimeMillis() - time;
    return time;
  }

  private long testNewBufferCorrectSize() {
    long time = System.currentTimeMillis();
    for (int i = 0; i < repeats; i++) {
      StringBuffer buf = new StringBuffer(38890);
      for (int j = 0; j < UPTO; j++) {
        buf.append(j);
      }
      buf.toString();
    }
    time = System.currentTimeMillis() - time;
    return time;
  }

  private long testExistingBuffer() {
    StringBuffer buf = new StringBuffer();
    long time = System.currentTimeMillis();
    for (int i = 0; i < repeats; i++) {
      buf.setLength(0);
      for (int j = 0; j < UPTO; j++) {
        buf.append(j);
      }
      buf.toString();
    }
    time = System.currentTimeMillis() - time;
    return time;
  }

  public String testAll() {
    return testNewBufferDefault() + "," +
        testNewBufferCorrectSize() + "," + testExistingBuffer();
  }

  public static void main(String[] args) {
    System.out.print(System.getProperty("java.version") + ",");
    System.out.print(System.getProperty("java.vm.name") + ",");
    // warm up the hotspot compiler
    new StringBufferTest(10).testAll();
    System.out.println(new StringBufferTest(400).testAll());
  }
}

Initialising Singletons

I discovered this one by pure chance, and I suspect that it is a bug in the server hotspot compiler. When I initialise Singletons, I usually do so in the static initialiser block. For example:

public class Singleton1 {
  private static final Singleton1 instance = new Singleton1();
  public static Singleton1 getInstance() {
    return instance;
  }
  private Singleton1() {}
}

Sometimes, I will initialise it in a synchronized block inside the getInstance() method. This is necessary when we combine the Singleton with polymorphism, and when we therefore want to choose which subclass to use. The code would then be:

public class Singleton2 {
  private static Singleton2 instance;
  // lazy initialization
  public static Singleton2 getInstance() {
    synchronized (Singleton2.class) {
      if (instance == null) {
        instance = new Singleton2();
      }
    }
    return instance;
  }
  private Singleton2() {}
}

However, thanks to byte code reordering by the hotspot compiler, I would avoid using the double-checked locking approach, otherwise I might return a half-initialised object to the caller of getInstance(). So I would not write it like this (since Java 5, the correct way would be to make the instance field volatile):

public class Singleton3 {
  private static Singleton3 instance;
  // double-checked locking - broken in Java - don't use it!
  public static Singleton3 getInstance() {
    if (instance == null) {
      synchronized (Singleton3.class) {
        if (instance == null) {
          instance = new Singleton3();
        }
      }
    }
    return instance;
  }
  private Singleton3() {}
}

The table below contains the relative speed between calling getInstance() on each of the Singletons. Note the outliers marked in red. JDK 1.4.0_04 client hotspot for some reason performs really badly for the double-checked locking example. On the other hand, this particular test is terrible for the server hotspot in JDK 1.5.0_02. The Singleton1 is 1000 times slower in JDK 1.5.0 than in JDK 1.4.0, without changing a single line of code, and without even compiling anew. It is surprises like these that can make your project suddenly perform awefully.

Java Version Hotspot Type Singleton1 Singleton2 Singleton3
1.3.1_12 Client 220 1923 851
1.3.1_12 Server 220 1883 971
1.4.0_04 Client 360 1923 6339
1.4.0_04 Server 10 1633 530
1.4.1_07 Client 340 2013 1562
1.4.1_07 Server 20 1593 530
1.4.2_05 Client 330 1983 1632
1.4.2_05 Server 20 1783 631
1.5.0_02 Client 290 2144 1261
1.5.0_02 Server 10385 10455 8962

My explanation for these results is that either in the Server VM the access to the instance is not inlined in JDK 1.5, or it is simply a bug in the server hotspot compiler. If we compare the speed of using interpreted mode vs. mixed mode, we can see that the interpreted speed is similar to the server hotspot VM:

Java Version Hotspot Type Singleton1 Singleton2 Singleton3
1.5.0_02 Client Interpreted 10415 14962 9423
1.5.0_02 Server Interpreted 7742 21611 9543

For completeness, here is the code for SingletonTest:

public class SingletonTest {
  private static final int UPTO = 100 * 1000 * 1000;
  public static void main(String[] args) {
    System.out.print(System.getProperty("java.version") + ",");
    System.out.print(System.getProperty("java.vm.name") + ",");

    long time;

    time = System.currentTimeMillis();
    for (int i = 0; i < UPTO; i++) {
      Singleton1.getInstance();
    }
    time = System.currentTimeMillis() - time;
    System.out.print(time + ",");

    time = System.currentTimeMillis();
    for (int i = 0; i < UPTO; i++) {
      Singleton2.getInstance();
    }
    time = System.currentTimeMillis() - time;
    System.out.print(time + ",");

    time = System.currentTimeMillis();
    for (int i = 0; i < UPTO; i++) {
      Singleton3.getInstance();
    }
    time = System.currentTimeMillis() - time;
    System.out.println(time);
  }
}

Update: In modern JVMs, we have something called biased locking, which means that tests of synchronized really need to be written with multiple threads calling the code. At the time of writing, this was ok, but in 2014 I would recommend using the Java Microbenchmarking Harness. See also Issue 217 and Issue 217b.

Keep coding, and don't leave performance tests for too late in the game.

Kind regards

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...