- 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
An optimizing compiler should determine if there is a computationally cheaper alternative to a set of instructions and replace those slower instructions with the faster alternative.
The classic version of this technique, termed strength reduction, replaces an operation with an equivalent operation that is faster. Consider the following lines of code:
- 64 -
x = x + 5; y = x2;
z = x 4;
These lines can be replaced by faster operations without altering the meaning of any statements:
x += 5; Assignment in place is faster y = x 1; each right shift by one place is equivalent to dividing by 2
z = x 2; each left shift by one place is equivalent to multiplying by 2
These examples are the most common cases of strength reduction. All the shorthand arithmetic operators
++
,
--
,
+=
,
-=
,
=
,
=
,
|=
,
=
are computationally faster than their nonshorthand expansions, and should be used by the coder or replaced by the compiler where appropriate.
[5] [5]
One of the technical reviewers for this book, Ethan Henry, has pointed out to me that there is no actual guarantee that these strength reductions are more efficient in Java. This is true. However, they seem to work for at least some VMs. In addition, compilers producing native code including JIT compilers
should produce faster code, as these techniques do work at the machine-code level.
3.4.2.5 Replace runtime computations with compiled results
An optimizing compiler can identify code that requires runtime execution if bytecodes are directly generated, but can be replaced by computing the result of that code during the compilation phase.
The result can then replace the code.
This technique is applied by most compilers for the simple case of literal folding see Section
3.5.1.1 and
Section 3.5.1.2 . And it can be extended to other structures by adding some semantic
input to the compiler. A simple example is:
String S_NINETY = 90; int I_NINETY = Integer.parseIntS_NINETY;
Although it is unlikely that anyone would do exactly this, similar kinds of initializations are used. An optimizing compiler that understood what
Integer.parseInt
did could calculate the resulting
int
value and insert that result directly into the compiled file, thus avoiding the runtime calculation.
3.4.2.6 Remove unused fields