| ## Java 1.1 Unleashed # - 30 - Optimizing Java Code by Michael Morrison IN THIS CHAPTER What Is Code Optimization? Optimizing with the JDK Compiler Understanding the Costs of Common Operations Isolating Sluggish Code Implementing Optimization Strategies Even with all Java's power and wonder, few people can argue the primary drawback to Java as a development platform: execution speed. It's certainly true that the execution speed of Java programs has improved dramatically with the advent of just-in-time (JIT) compilers, but there is still room for improvement. Rather than wait around for faster JIT compilers or enhanced Java virtual machines, you can empower yourself by taking control of your Java code and the speed at which it executes. This chapter looks at Java code optimization and how you can improve the performance of your Java programs. In this chapter, you learn various optimization techniques that can help you become a more efficient Java programmer. To really understand how these optimization techniques help, you have to go under the hood of Java a little. However, I think you'll find that Java code optimization isn't all that difficult. So roll up your sleeves and prepare to get a little dirty. ## What Is Code Optimization? Code optimization is the process of modifying working code to a more optimal state based on a particular goal. The fact that optimization takes place on working code is an important point; always perform optimizations on code after you get the code working. Also make sure that you retest the code after validating your optimizations. The type of optimization performed depends on the desired goal; code optimization can be divided into three distinct types, which are based on the needs of the developer: Maintainability Size Speed Based on this list, you probably realize now that code optimization isn't just about improving performance; speed optimization is simply one type of optimization. Nevertheless, it is usually the most important type of optimization to consider when dealing with Java code. ### Maintainability Optimization Maintainability optimization is performed to help make code more manageable in the future. This type of optimization is usually geared toward the structure and organization of code rather than modifications to the algorithms used in the code. In general, maintainability optimization involves a programmer's studying the code at large and making changes to help other programmers understand and modify the code in the future. Fortunately, the rigid structure of the Java language goes a long way toward keeping things optimized for maintainability. In fact, if you adhere to basic object-oriented design principles, you really don't need to do anything else to keep your code optimized for maintainability. ### Size Optimization Another popular optimization is size optimization, which involves making changes to code that result in a smaller executable class file. The cornerstone of size optimization is code reuse, which comes in the form of inheritance for Java classes. Fortunately, good object-oriented design strategies naturally favor size optimization, so you rarely have to go out of your way to perform this type of optimization. For example, it's simply good design practice to put code that is reused more than once into a method. In this way, most size optimizations naturally take place during the initial code development. Although it's not typically a huge issue, size optimization can't be completely ignored in Java programming. This is because the size of your compiled Java classes directly impacts the amount of time it takes your program to load and initially execute--especially over an Internet connection. If you leverage as much of the standard Java API code as possible and reuse code by deriving from other classes, you're probably doing enough for the cause of reducing class size. ### Speed Optimization Speed optimization is without a doubt the most important type of optimization when it comes to Java programming. Speed optimization includes all the techniques and tricks used to speed up the execution of code. Considering the performance problems inherent in Java, speed optimization takes on an even more important role in Java than it does in other languages such as C and C++. Because the Java compiler has the last word on how code is generated, most speed optimizations are performed with the compiler in mind. The rest of this chapter focuses on issues of speed optimization and how to get the best performance out of your Java code. At times, you will sacrifice the other types of optimization for the sake of speed. In most cases, this sacrifice is entirely acceptable--even expected--because the organization of the code and size of the executable classes won't matter much if your programs are too slow to be useable. ## Optimizing with the JDK Compiler All optimizations begin and end with the Java compiler. If you don't understand the compiler, you're largely guessing at which optimizations will have a positive effect on your code. So let's take a look at the JDK compiler and see what role it plays in turning out speedy Java bytecodes. Bytecodes comprise the intermediate processor-independent code generated by the Java compiler. Bytecode executables (classes) are interpreted by the Java runtime system. NOTE: Third-party Java compilers are being released that outclass the JDK compiler in regard to speed optimization. Nevertheless, the JDK compiler is the standard Java compiler and is currently the most reliable. The JDK compiler (`javac`) includes a switch for generating optimized Java bytecode executables: `-O`. In the JDK version 1.1, this switch results in only two optimizations taking place: inline methods and exclusion of line numbers. The first of these optimizations is the only one that affects the speed of the executable bytecode; `final`, `static`, and `private` methods are inlined by the compiler, resulting in less method call overhead. Method inlining is the process of replacing each call to a method with the actual method code. Inlining can often increase the size of the resulting class file, but it can help improve performance. The second optimization performed by the JDK compiler results in the exclusion of line number information from the executable class file. This is a size optimization and does nothing in terms of helping speed up the code. However, it does have the side benefit of making the executable class files a little smaller, which improves the download time of Java applets. The truth is that the JDK compiler does little for you in regard to optimization. This means that you have to plan on doing a lot of optimization by hand. Although this may sound like a lot of responsibility, keep in mind that any insight you have into the efficiency of your code will ultimately pay off regardless of how good the compiler is. In other words, knowing how to optimize code by hand is an extremely valuable skill. ## Understanding the Costs of Common Operations Now that you understand what the JDK compiler does (or doesn't do) for you in regard to optimization, it's time to focus on the Java runtime system. By examining the runtime system, you can get an idea of how fast certain types of code run and make smarter decisions about the way you write Java code. What do I mean by examining the runtime system? Well, I mean running different types of code and timing each type to see how the speeds match up. This operation gives you a very realistic look at how code differs in terms of execution speed, and consequently gives you a place to start making appropriate code optimizations. The speed of an operation is often referred to as the cost of the operation. Code optimization can almost be likened to accounting, in which you try to keep from blowing a performance budget with your code costs. Jonathan Hardwick developed a neat benchmark applet, called the Java Microbenchmark applet, for determining how long common Java operations take to execute. Tables 30.1 through 30.3 contain the results of running the Microbenchmark applet on my system with the JDK 1.1 applet viewer. These tables contain approximate times in microseconds for common Java operations. Incidentally, the system used to perform the cost analysis was a Pentium 100 running Windows 95. You can try the Microbenchmark applet yourself by visiting its Web site at `www.cs.cmu.edu/~jch/java/microbench.html`. #### Table 30.1. The costs of Java variable accesses (speeds measured in microseconds).
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
#### Table 30.2. The costs of increment with Java data types.
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
#### Table 30.3. The costs of miscellaneous Java operations.
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
These tables point out lots of interesting information regarding the performance of Java code. From Table 30.1, it's apparent that method variables are a little more efficient to use than instance variables. Furthermore, you can see that array element assignment is slower than method variable assignment because Java performs bounds checking operations whenever an array element is accessed. Keep in mind that this table isn't meant as an argument to get rid of all your class member data. Rather, think of it as providing insight into making decisions where the design could go either way. 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. ## Isolating Sluggish Code 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. ## Implementing Optimization Strategies 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. ### Rethink Algorithms 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. ### Use Native Methods 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." ### Use Inline Methods 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!) ### Replace Slow Java API Classes and Methods 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. ### Use Look-Up Tables 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. ### Eliminate Unnecessary Evaluations 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;
### Eliminate Common Subexpressions 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);
### Expand Loops 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. ### Avoid the String Concatenation Operator 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. ## Summary 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.