Thursday, June 28, 2007

Java Performance tips & Programming practice for Newbies

Off late, I have started working on Axion DB, a Java based embedded database engine. I was working on various performance improvements for Axion DB. I was amazed at the speed of Axion DB on JDK 5. I am was sure that Axion would be still faster on JDK 6. So, we started testing the database with large datasets. My machine is a Dell D620 laptop, duo core processor and 2 GB RAM. We tested with 1 million rows of CSV file, even then Axion didn't show any hiccups. So we tested the table creation in Axion for 10 million rows of CSV File(~4 min) and compared it with SQL Loader of Oracle (1 min and 5 sec). Then we realized that Axion was 4X slower than Oracle SQL Loader.

The first thought that came to our mind was whatever we do we can't beat Oracle SQL Loader, not even going to match their performance. They must be using C/C++ and this is Java and there's nothing much that can be done to help it. Out of curiosity, we started profiling the Axion to find the hot spots. When we profiled the application, then stuck the lightning. We were having severe performance bottleneck at the place where we parse each line and create the column value.

These were the reasons for the bottleneck:
1. Lot of method calls.
2. Lot of heavy weight object creation.
3. improper use of for loop.
4. IO bottleneck.
5. No multi threading.
6. unnecessary method calls.

From these, I learnt a few essential points. I will try to explain them each.

1. If you doing some low level related work where speed is absolutely important, then don't use the Java built-in data types. Say for example, you are using Integer class, just think of all the overhead you are creating by using the wrapper class. You should always use the primitives instead of wrapper if you want to achieve performance. How many of use consider using unsigned int or unsigned long in our day-to-day Java applications. We don't use it because Java application that you are creating is fast enough not to bother you any longer. So, you don't think twice before using the Java build-in wrapper classes. But in Database kind of scenario, you should be using Primitives to achieve speed and performance. They manipulate the bits directly which gives the max possible performance. We use Apache commons-primitives to achieve this. There must be couple of other open source primitives available. Trying using one of them.

2. Take a look at the following piece of code.

private long getLength(String stringObj) {
long len = 1;
for(int i = 0; i < stringObj.length(); i++) {
len++;
}
return len;
}

Ok, don't ask me why should you write this getLength() method for a string again. Its the simplest example that came to my mind immediately. Ok, back to the problem, can you find out what is the performance bottleneck here. This is a perfectly normal one that we write in almost every program. But this is such a big blunder that we are making. Think in terms of this method getting called for a very very big string. Ok, this is it. Take a look at the for() statement. The comparison section says, i < stringObj.length(). Assume this stringObj is of length 1 million. Then this i < stringObj.length() is going to be called for a million times. For each method call, your JVM is going to call the method, push the method related entry in the stack. Then after the method call, its going to pop out the method related meta data. So if this is done for a million times, think of the overhead.

If this kind of method calls are unavoidable, then thats a different issue altogether. But check the above method. Its very clear that we need the length of the string object to compare with the current index. So why not do it like this.

private long getLength(String stringObj) {
long len = 1;
for(int i = 0, I = stringObj.length(); i < I; i++) {
len++;
}
return len;
}

Ok, now look at the method. Now you see that the length() method is called only once at the beginning of the for loop. This is not going to be called a million times. This certainly improves the performance by 2X to 3X times. This 2X to 3X time improvement is going to make a big impact in case of processing a million row.

Now is there any other optimization that can be made to this. Go through the method again. Assume that this particular getLength() method is going to be called a million times. Now think of this again. If you check the method, you have a len object which holds the length. you loop through the object and find the length and return the length object finally. Ok, let me write this method in a different way.

private long getLength(String stringObj) {
long len = 1;
for(int I = stringObj.length(); len <= I ; len++);
return len;
}

Now if you see, the variable 'i' used as a index is eliminated. I dont mean to say, you are always doing this kind of mistake. I just mean to say, if we code with proper care, then Java can give you really good performance.

3. Third biggest point is allocate memory judicially. Just because Java automatically does cleanup, it doesn't mean that we can program anything and expect the JVM to do the optimization for us. Of course, JVM does the optimization but still why do you want to leave it to others when you can do it for yourself. If you sure that you dont need a long, dont use it. Use int. If you feel you dont need an int, then use short. Think twice before you declare a variable.

4. Take a look at the following code.

private String getString(char[] charArray) {
for(each character) {
if(isDelimiterChar(character)){
// do something.
}
if(isQuoteChar(character)) {
// do something.
}
if(isEOF(character)){
// do something
}
}
}

boolean isDelimiter(char c) {
return (c == ',');
}

boolean isEOF(char c) {
return (c == -1);
}

boolean isQuoted(char c) {
return (c == '\"');
}

Now, Assume this piece of code is going to be called for a million times. Then think of this code again. Object orientation in Java helps us tremendously. It helps in modularization, code reusability, etc. But all those things are not always useful. If you coding for performance, then rethink about that. Its always better to write Inline code where ever possible since it reduces the overhead of method call - like push, pop into the method stack, etc. So if you see the above piece of code, the method isQuoted checks if the given character is double quote character, method isEOF checks if char value is -1, method isDelimiter checks if char is equal to comma character. So this is not something that we have to do it in method. Its not a complex piece of code that can be made as a method so that this method can be reused easily. It can be directly inlined as given below.

private String getString(char[] charArray) {
for(each character) {
// check for comma character
if(character == ','){
// do something.
}

// check for quote characted
if(character == '\"') {
// do something.
}

// check for EOF
if(character == -1){
// do something
}
}
}

Though the above doesn't look as elegant as the other one, still this one is going to be faster than the other piece of code. So keep in mind, elegant code is not always the best performing code.

5. Don't take risks that you can easily avoid. In case of coding an application that needs high degree of reliability, don't take risks. Check the following piece of code.

private boolean isNullString(String string) {
return (string.equals(""));
}

So, this looks like a absolutely normal piece of code. But still there's a hidden trap here. What is the string which is passed is null. So try to rephrase the code as given below.

private boolean isNullString(String string) {
return ("".equals(string));
}

This should work now without any null pointer exceptions.

6. Of course, IO is always a performance issue. But now in Java with NIO, its really simple and efficient to do IO related tasks. Always use buffering if you need performance. Because if you directly use FileInputStream, JVM is going to issue file read system call everytime you read a byte of data. In case of buffering, whole buffer is read in one go and only if there's no data in the buffer, JVM issues a system call to read the disk.

7. Always synchronize on a lock before waiting on the lock. Also, try to use wait with time. Else your thread might wait indefinitely, if there's no one else to notify this thread. Also use wait in a loop and check on a condition which is expected to be updated by other threads.

We also parallelized the file read and used multiple threads to read the same file in a faster manner. After all these performance tuning in Axion DB, we could finally create the table for 10 million rows of CSV file in flat 45 Seconds! Can you believe it! Even I am Awe Stuck! I can vouch that if you code carefully and judiciously you can almost match C/C++ performance(Of course, its hard to exceed compiled code(C/C++) performance with interpreted(Java) code) with all the hotspot GC and other advanced technologies available in Java.

As a newbie in the industry, these were certainly few of the best programming practice that I learnt from my experience at Sun. I was committing almost all of these mistakes when I came out of the college. It helped me improve the application performance tremendously. Hope this post helps other newbies!

3 comments:

  1. Good one Karthi and will be useful.
    Don't you think the obj.length() method called a million times be optimized by the compiler if there was no chane to the obj properties in the mean time. This is too much to ask. Just I wanted to know...

    We have read abt compiler optimizations. Hope such optimizations are handled in the Java->Object code generation phase.I would like to know what possible optimizations happen during Interpretation and subsequent execution

    ReplyDelete
  2. yes, there will compiler optimization to get you the object length faster. But still method has its own overhead of pushing in & popping out methods in and out of the stack space. So, its better to inline that one. When we tested Axion DB after removing these kinds of method calls, performance improved by 6 seconds when operating on a CSV file with 1 million rows. If you are absolutely concerned about your code speed & performance, this really helps a lot.

    ReplyDelete
  3. Good post buddy! Lot of take aways in them.

    But you call call this as a punch dialog, "I was committing almost all of these mistakes when I came out of the college" Nice practice and will try to follow them.

    ReplyDelete