|
JDK 1.5 - Performance Surprises in Tiger
Java 5.0, code is also called "Tiger", which promises to be the most significant new version of Java since the introduction of the language.
This articles shows you the performance enhancements of JDK 1.5.
2005-03-28 The Java Specialists' Newsletter [Issue 105] - Performance Surprises in Tiger
Author:
Dr. Heinz M. Kabutz JDK version: Sun JDKs 1.3.1_12, 1.4.0_04, 1.4.1_07, 1.4.2_05, 1.5.0_02
If you are reading this, and have not subscribed, please consider doing it now by going to our
subscribe
page. You can subscribe either via email or RSS.
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.
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 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:
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);
}
}
Keep coding, and don't leave performance tests for too late
in the game.
Kind regards
Heinz
This material from The Java(tm)
Specialists' Newsletter by Maximum Solutions (South Africa). Please contact Maximum
Solutions for more information.
|