- 62 -
3.4.1 What Optimizing Compilers Cannot Do
Optimizing compilers cannot change your code to use a better algorithm . If you are using an inefficient search routine, there may be hugely better search algorithms giving orders of magnitude
speedups. But the optimizing compiler only tries to speed up the algorithm you are using with a probable small incremental speedup. It is still important to profile applications to identify
bottlenecks even if you intend to use an optimizing compiler.
It is important to start using an optimizing compiler from the early stages of development in order to tailor your code to its restrictions. More than one project I know of has found the cost of trying to
integrate an optimizing compiler at a late stage of development too expensive. In these cases, it means restructuring core routines and many disparate method calls, and can even require some
redesign to work around limitations imposed by being unable to correctly handle reflection and runtime class resolution. Optimizing compilers have difficulty dealing with classes that cannot be
identified at compile time e.g., building a string at runtime and loading a class of that name. Basically, using
Class.forName
is not and cannot be handled in any complete way, though several compilers try to manage as best they can. In short, managers with projects at a late stage of
development are often reluctant to make extensive changes to either the development environment or the code. While code tuning can be targeted at bottlenecks and so normally affects only small
sections of code, integrating an optimizing compiler can affect the entire project. If there are too many problems in this integration, most project managers decide that the potential risks outweigh
the possible benefits and prefer to take the safe route of carrying on without the optimizing compiler.
3.4.2 What Optimizing Compilers Can Do
Compilers can apply many classic optimizations and a host of newer optimizations that apply specifically to object-oriented programs and languages with virtual machines. I list many
optimizations in the following sections.
You can apply most classic compiler-optimization techniques by hand directly to the source. But usually you should not, as it makes the code more complicated to read and maintain. Individually,
each of these optimizations improves performance only by small amounts. Collectively as applied by a compiler across all the code, they can make a significant contribution to improving
performance. This is important to remember: as you look at each individual optimization, in many cases the thought, Well, that isnt going to make much difference, may cross your mind. This is
correct. The power of optimizing compilers comes in applying many small optimizations automatically that would be annoying or confusing to apply by hand. The combination of all those
small optimizations can add up to a big speedup.
Optimizing-compiler vendors claim to see significant speedups: up to 50 for many applications. Most applications in serious need of optimization are looking for speedups even greater than this,
but dont ignore the optimizing compiler for that reason: it may be doubling the speed of your application for a relatively cheap investment. As long as you do not need to restructure much code
to take advantage of them, optimizing compilers can give you the biggest bang for your buck after JIT VMs in terms of performance improvements.
The next sections list many of the well-known optimizations these compilers can apply. This list can help you when selecting optimizing compilers, and also can help if you decide you need to
apply some of these optimizations by hand.
3.4.2.1 Remove unused methods and classes
- 63 - When all application classes are known at compile time, an optimizing compiler can analyze the full
runtime code-path tree, identifying all classes that can be used and all methods that can be called. Most method calls in Java necessarily invoke one of a limited set of methods, and by analyzing the
runtime path, you can eliminate all but one of the possibilities. The compiler can then remove unused methods and classes. This can include removing superclass methods that have been
overridden in a subclass and are never called in the superclass. The optimization makes for smaller download requirements for programs sent across a network and, more usefully, reduces the impact
of method lookup at runtime by eliminating unused alternative methods.
3.4.2.2 Increase statically bound calls
An optimizing compiler can determine at compile time whether particular method invocations are necessarily polymorphic and so must have the actual method target determined at runtime, or
whether the target for a particular method call can be identified at compile time. Many method calls that apparently need to have the target decided at runtime can, in fact, be uniquely identified see
the previous section. Once identified, the method invocation can be compiled as a static invocation, which is faster than a dynamic lookup. Static methods are statically bound in Java. The following
example produces in superclass if
method1
and
method2
are static, but in subclass if
method1
and
method2
are not static:
public class Superclass { public static void mainString[] args {new Subclass .method1 ;}
public static void method1 {method2 ;} public static void method2 {System.out.printlnin superclass ;}
} class Subclass extends Superclass {
public static void method2 {System.out.printlnin subclass ;} }
3.4.2.3 Cut dead code and unnecessary instructions, including checks for null
Section 14.9 of the Java specification requires compilers to carry out flow analysis on the code to determine the reachability of any section of code. The only valid unreachable code is the
consequence of an
if
statement see Section 3.5.1.4
. Invalid unreachable code must be flagged as a compile error, but the valid code from an
if
statement is not a compile error and can be eliminated. The
if
statement test can also be eliminated if the boolean result is conclusively identified at compile time. In fact, this is a standard capability of almost all current compilers.
This flow analysis can be extended to determine if other sections and code branches that are syntactically valid are actually semantically unreachable. A typical example is testing for
null
. Some null tests can be eliminated by establishing that the variable has either definitely been
assigned to or definitely never been assigned to before the test is reached. Similarly, some bytecode instructions that can be generated may be unnecessary after establishing the flow of control, and
these can also be eliminated.
3.4.2.4 Use computationally cheaper alternatives strength reduction