Java Specialists' Java Training Europehome of the java specialists' newsletter

The Java Specialists' Newsletter
Issue 1052005-03-28 Category: Performance Java version: Sun JDKs 1.3.1_12, 1.4.0_04, 1.4.1_07, 1.4.2_05, 1.5.0_02

GitHub Subscribe Free RSS Feed

Performance Surprises in Tiger

by Dr. Heinz M. Kabutz

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.

Oh, on another note, I spoke at an event organised by ITWeb in South Africa. If you want to see what I look and sound like under pressure, check this out. I tried to be reasonably coherent on the video clip, but that short video clip was completely unrehearsed, and if I had had time to prepare, I would have said something else.

Join us on Crete (or via webinar) for advanced Core Java Courses:Concurrency Specialists Course 1-4 April 2014 and Java Specialists Master Course 20-23 May 2014.

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 VersionHotspot TypeSB1SB2SB3
1.4.0_04Client165314521632
1.4.0_04Server1082951 1072
1.4.1_07Client175215821723
1.4.1_07Server1101962 1061
1.4.2_05Client1072871 1071
1.4.2_05Server630 551 681
1.5.0_02Client1032501 490
1.5.0_02Server811 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 VersionHotspot TypeSB1SB2SB3
1.5.0_02Client921441431
1.5.0_02Server721350341

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 final static 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 VersionHotspot TypeSingleton1Singleton2Singleton3
1.3.1_12Client220 1923 851
1.3.1_12Server220 1883 971
1.4.0_04Client360 1923 6339
1.4.0_04Server10 1633 530
1.4.1_07Client340 2013 1562
1.4.1_07Server20 1593 530
1.4.2_05Client330 1983 1632
1.4.2_05Server20 1783 631
1.5.0_02Client290 2144 1261
1.5.0_02Server10385 104558962

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 VersionHotspot TypeSingleton1Singleton2Singleton3
1.5.0_02Client Interpreted10415149629423
1.5.0_02Server Interpreted7742 216119543

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

Performance Articles Related Java Course

Java Master
Java Concurrency
Design Patterns
In-House Courses



© 2010-2014 Heinz Kabutz - All Rights Reserved Sitemap
Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners. JavaSpecialists.eu is not connected to Oracle, Inc. and is not sponsored by Oracle, Inc.
@CORE_THE_BAND #RBBJGR