Java 1.1 Unleashed
- 30 -
|
Description | Operation | Cost |
Method variable | ||
assignment | i = 1; | 0.14 |
Instance variable | ||
assignment | this.i = 1; | 0.19 |
Array element | ||
assignment | a[0] = 1; | 0.29 |
Description | Operation | Cost |
Byte variable | ||
increment | byte b++; | 0.28 |
Short variable | ||
increment | short s++; | 0.28 |
Int variable | ||
increment | int i++; | 0.11 |
Long variable | ||
increment | long l++; | 0.30 |
Float variable | ||
increment | float f++; | 0.34 |
Double variable | ||
increment | double d++; | 0.53 |
Description | Operation | Cost |
Object creation | new Object(); | 6.6 |
Method invocation | null_func(); | 0.79 |
Synchronized method | sync_func(); | 5.8 |
Math function | Math.abs(x); | 3.2 |
Equivalent math code | (x < 0) ? -x : x; | 0.17 |
Table 30.2 shows timing data related to the use of the standard Java data types. As you may have expected, the two 32-bit data types (int and float) showed the best performance because the tests were performed on a 32-bit system.
Even though the floating-point types show comparable performance to the integer types, don't be misled about using integer math over floating-point math. This timing table reflects only an increment operation, which is much different than more complex operations performed in the context of a practical Java program. Integer math is much more efficient than floating-point math. So use the table as a measure of the relative speeds among integer types, and then try to use integer math throughout your code.
Table 30.3 focuses on a few miscellaneous operations that are worth thinking about. First off, it shows the high cost of creating an object. This should serve as an incentive to eliminate the creation of temporary objects within a loop where the creation occurs over and over. Rather, you can place the temporary object above the loop and reinitialize its members as needed inside the loop.
Table 30.3 also shows the dramatic performance costs of using a normal method versus a synchronized method. Even though synchronization is very important in multithreaded programming, Table 30.3 should be some encouragement to minimize the usage of synchronized methods in situations where you're in a performance squeeze.
Finally, Table 30.3 shows you how using the standard Java math methods can sometimes be a burden. Even something as simple as taking the absolute value of a number imposes much greater performance costs when you call the Math.abs() method, as opposed to inlining the equivalent code yourself.
NOTE: It's worth noting that the performance of Java code in Java 1.1 has improved considerably over that of Java version 1.02. Various changes were made within the Java runtime system to improve the overall performance of Java code. For example, the Java 1.1 runtime system automatically discards unused classes, which enables it to operate more efficiently and with less memory.
The biggest mistake you can make in regard to optimizing your Java code is trying to optimize all the code. Being smart about what code you attack is crucial in not spending years trying to improve the performance of your code. More important, it's a well-established fact that a relatively small portion of code is usually responsible for the bulk of the performance drain. It's your job to isolate this code and then focus your optimization efforts accordingly.
Fortunately, isolating problem code isn't all that difficult if you use the proper tools. The most useful tool in finding bottlenecks in your code is a profiler. A profiler's job is to report the amount of time spent in each section of code as a program is executing. The Java runtime interpreter has an undocumented built-in profiler that is easy to use and works pretty well. To use the runtime interpreter profiler, simply specify the -prof option when using the interpreter, like this:
java -prof Classname
Classname is the name of the class you want to profile. Of course, this technique doesn't work too well for applets, because they must be run within the context of the applet viewer tool or a Web browser. Fortunately, you can use the profiler with applets by altering the arguments to the interpreter a little, like this:
java -prof sun.applet.AppletViewer Filename
In this case, Filename is the name of the HTML file containing a link to your applet. When you finish running the applet, the interpreter writes a file named java.prof to the current directory. This file contains profile information for the applet you just ran. The information consists of a list of methods sorted according to how many times each method is called over the course of the program. The methods are listed in order of decreasing method calls, meaning that the first method in the list is called more than any other method.
You can easily use this information as a guide to determine the code on which to focus your optimization efforts. The methods appearing at the top of the java.prof file should receive more of your attention because they are being called much more than methods farther down in the list. Making small performance gains in a method called 20,000 times has a much greater impact than speeding up a method that is called only a couple hundred times. The cool thing is that you can try different optimizations and then run the profiler again to see whether the relative times have changed. This is a very practical (if somewhat time-consuming) way to make great strides in speeding up your code.
Now that you've isolated the code that is making your program crawl, it's time to look into exactly what optimizations you can perform to speed things up. You won't always be able to optimize every piece of problem code; the goal is to make big dents in the areas that can be optimized.
Many C/C++ programmers have traditionally resorted to assembler language when the issue of performance is raised. As a Java programmer, you don't have this option. This is actually a good thing because it forces you to take a closer look at your design approach instead of relying on heavier processor dependence to solve your problems. What the assembly heads don't realize is that much more significant gains can be made by entirely rethinking an algorithm than by porting it to assembly. And trust me, the amount of time spent hand-coding tedious assembly can easily result in a leaner, more efficient algorithm.
This same ideology applies to Java programming. Before you run off writing native methods and expanding loops to get every little ounce of performance (which you learn about in the next section), take a step back and see whether the algorithm itself has any weaknesses. To put this all into perspective, imagine if programmers had always resorted to optimizing the traditional bubble sort algorithm and had never thought twice about the algorithm itself. The quick sort algorithm, which is orders of magnitude faster than the bubble sort without any optimization, would never have come about.
I hate to recommend them, but the truth is that native methods (methods written in C or C++ that can be called from Java code) are typically much faster than Java methods. The reason I'm reluctant to promote their use is that they blow the platform-independence benefit of using Java, therefore tying your program to a particular platform. Additionally, you can't use native methods in applets, because native code imposes all kinds of security problems. If platform independence isn't high on your list and you aren't developing an applet, however, by all means look into rewriting problem methods in C. You learn how to connect Java to C code in Chapter 32, "Integrating Native Code with the Native Method Interface."
Inline methods, whose bodies appear in place of each method call, are a fairly effective way of improving performance. Because the Java compiler already inlines final, static, and private methods when you have the optimization switch turned on, your best bet is to try to make as many methods as possible final, static, or private. If this isn't possible and you still want the benefits of inlined code, you can always inline methods by hand: Just paste the body of the method at each place where it is called. This is one of those cases in which you are sacrificing both maintainability and size for speed. (The things we do for speed!)
There may be times when you are using a standard Java API class for a few of its features, but the extra baggage imposed by the generic design of the class is slowing you down. In situations like this, you may be better off writing your own class that performs the exact functionality you need and no more. This streamlined approach can pay off big, even though it comes at the cost of rewriting code that already works.
Another similar situation occurs when you are using a Java API class and you isolate a particular method in it that is dragging down performance. In this situation, instead of rewriting the entire class, just derive from it and override the troublesome method. This is a good middle-of-the-road solution because you leverage code reuse against performance in a reasonable manner.
An established trick up the sleeve of every programmer who has wrestled with floating-point performance problems is the look-up table. Look-up tables are tables of constant integer values that are used in place of time-consuming calculations. For example, a very popular type of look-up table is one containing values for trigonometric functions, such as sine. The use of trigonometric functions is sometimes unavoidable in certain types of programs. If you haven't noticed, Java's trigonometric functions are all floating-point in nature, which is a bad thing in terms of performance. The solution is to write an integer version of the desired function using a look-up table of values. This relatively simple change is sometimes a necessity considering the performance hit you take by using floating-point math.
Moving along into more detailed optimizations, you can often find unnecessary evaluations in your code that serve only to eat up extra processor time. Following is an example of some code that unnecessarily performs an evaluation that acts effectively as a constant:
for (int i = 0; i < size(); i++) a = (b + c) / i;
The addition of b + c, although itself a pretty efficient piece of code, is being calculated each time through the loop. It is better off being calculated before the loop, like this:
int tmp = b + c; for (int i = 0; i < size(); i++) a = tmp / i;
This simple change can have fairly dramatic effects, depending on how many times the loop is iterated. Speaking of the loop, there's another optimization you might have missed. Notice that size() is a method call, which should bring to mind the costs involved in calling a method (you learned about this performance drag earlier in the chapter). You may not realize it, but size() is called every time through the loop as part of the conditional loop expression. The same technique used to eliminate the unnecessary addition operation can be used to fix this problem. Check out the resulting code:
int s = size; int tmp = b + c; for (int i = 0; i < s; i++) a = tmp / i;
Sometimes, you may be reusing a costly subexpression without even realizing it. In the heat of programming, it's easy to reuse common subexpressions instead of storing them in a temporary variable, like this:
b = Math.abs(a) * c; d = e / (Math.abs(a) + b);
The multiple calls to Math.abs() are costly compared to calling it once and using a temporary variable, like this:
int tmp = Meth.abs(a); b = tmp * c; d = e / (tmp + b);
One optimization that is popular among C/C++ programmers is loop expansion, or loop unrolling, which is the process of expanding a loop to get rid of the overhead involved in maintaining the loop. You may be wondering exactly what overhead I'm talking about. Well, even a simple counting loop has the overhead of performing a comparison and an increment each time through. This may not seem like much, but when you consider that some code is called thousands of times in a program, it's easy to see how small changes can sometimes yield big results.
Loop expansion basically involves replacing a loop with the brute-force equivalent. To better understand this process, consider the following piece of code:
for (int i = 0; i < 1000; i++) a[i] = 25;
That probably looks like some pretty efficient code and, in fact, it is. But if you want to go the extra distance and perform a loop expansion on it, here's one approach:
int i = 0; for (int j = 0; j < 100; j++) { a[i++] = 25; a[i++] = 25; a[i++] = 25; a[i++] = 25; a[i++] = 25; a[i++] = 25; a[i++] = 25; a[i++] = 25; a[i++] = 25; a[i++] = 25; }
In this code, you've reduced the loop overhead by an order of magnitude, but you've introduced some new overhead by having to increment the new index variable inside the loop. Overall, this code does outperform the original code, but don't expect any miracles. Loop expansion can be effective at times, but I don't recommend placing it too high on your list of optimization tricks.
Although it may look innocent, the string concatenation operator (+) is actually quite costly in terms of performance. It is costly because it results in a call to String.append(), which copies the two strings being concatenated into a string buffer using System.arrayCopy() and returns the result with a call to StringBuffer.toString(). This sluggish overhead can be eliminated by working with a single string buffer to begin with.
This chapter covered a somewhat murky area of Java programming: code optimization. You began by learning about the fundamental types of optimization, including the most popular type of Java optimization: speed optimization. You then learned about the optimizations (or lack thereof) provided by the JDK compiler. From there, you got a little dose of realism by looking into the timing costs of common Java operations. You finished the chapter by taking an in-depth look at some practical code optimizations you can apply to your own Java programs.
This chapter rounds out Part VI of this book, "Programming Strategies." Armed with these strategies, you're no doubt ready to press on into Part VII, "Advanced Java." In Part VII, you learn about all kinds of advanced Java programming issues including persistence, security, and reflection, among other things.
©Copyright, Macmillan Computer Publishing. All rights reserved.