Inlining
One of the strengths of Java and other object-orientated programming languages is their ability to
modularize and reuse code in the form of packages, classes and methods.
Strategy: Aggressively inline method calls
Currently, the unit of compilation that the JIT compiler considers is a single method. To make
optimizations more effective, the JIT compiler aggregates small methods into larger ones through
effective inlining. Inlining is a technique where a method call is replaced with the target method’s
bytecodes. This technique increases the scope of compilation beyond a single method. Inlining is also
a powerful way to propagate information into target methods, thereby reducing uncertainty in the
application code and making optimizations more effective.
Virtual and interface methods
Virtual and interface methods in Java provide a useful abstraction for developers. However, this
abstraction comes with an associated performance cost, because polymorphic calls might have multiple
target implementations during the execution of the application. Furthermore, the uncertainty of the target
method at these call sites can limit the optimizations that the JIT compiler performs, such as inlining.
Strategy: Devirtualize as many call sites as possible
However, the TRJIT compiler tries to overcome such constraints by using devirtualization techniques,
whereby a single-target implementation is chosen and the JIT transforms the virtual call to a direct
call. The JIT can use profiling or class hierarchy analysis [5] to detect that the receiver of a virtual call
is predominantly of a certain type. The transformation needs to be done with a virtual-guard test in
place to ensure that the correct target method is invoked in all cases for functional correctness.
Figure 1 shows an example of this transformation.
o = receiver object ;
x = receiver class (o) ;
if ( x == expected-class ) { // virtual guard
x.foo(a, b, c) ;
// direct call can be inlined
} else {
o.foo(a, b, c) ;
// guard failed, virtual call
}
Figure 1: Example of Devirtualization
Certain guards can be removed if they are proven to be unnecessary, based on the current class
hierarchy during the execution of the application. If a new class is loaded during the execution of the
application that changes the hierarchy of the class involved in a guard, then the TRJIT reinserts the
guard to maintain correct behavior.
IBM Just-In-Time Compiler (JIT) for Java
Best practices and coding guidelines for improving performance
Heap allocations
Java allocates objects on the heap. Therefore, references to objects might need to go through multiple
levels of the memory hierarchy, which includes the processor cache and main memory. Data locality is
important because hardware cache misses can lead to poor application performance. An application that
allocates many objects with a short lifespan can have reduced performance because of potential heap
fragmentation and poor locality, resulting in more hardware-cache misses. One of the ways in which the
TRJIT compiler helps data locality (and in turn, performance) is by converting heap allocations into stack
allocations when possible [3].
Strategy: Optimize for object locality
Using the stack for short-lived allocations leads to better locality because the stack is usually
available in the cache. This reduces memory latencies that result from cache misses. As an added
benefit, using the stack for short-lived allocations reduces the load on the garbage collector because
these allocations are no longer present in the heap.
The TRJIT compiler attempts to find allocations that never escape the method in which they are
allocated. See the example shown in Figure 2, where the allocation of the Integer object, intObj, is
local to method m1 and does not escape the method. The TRJIT compiler can convert this allocation
into a stack-allocation.
public class myClass {
public static void m1() {
Integer intObj = new Integer(100); //intObj does not escape m1
System.out.println(“Value is: “+ intObj.intValue());
}
}
Figure 2. intObj does not escape m1 and can be stack-allocated
However, in Figure 3, the allocation is stored into a field of another object and is, therefore, not local
to the method m1.
public class myClass {
public static void m1(myClass myObj) {
Integer intObj = new Integer(100);
myObj._myField = intObj;
// intObj escapes m1
System.out.println(“Value is: “+ intObj.intValue());
}
public Integer _myField;
}
Figure 3. intObj is stored into the field of another object, thus escaping, and cannot be stack-allocated
IBM Just-In-Time Compiler (JIT) for Java
Best practices and coding guidelines for improving performance
Java coding guidelines
This section presents a set of Java coding guidelines that are based on analyzing the performance of
various Java applications. The focus is on four areas with the most impact on application performance.
Object allocations
Object allocations are not cheap in Java because, after memory is allocated for an object, the object
needs to be initialized before it is accessible to the program. This involves zero-initializing the object fields
as well as invoking the constructor to perform initializations as specified by the user.
Guideline: Avoid creating objects inside loops
Objects that are allocated inside loops have a short lifespan and might not survive subsequent
iterations of the loop. For example, you can avoid creating objects inside loops to improve
performance of String concatenation operations. Although it is not obvious, the javac compiler
implicitly allocates a java/lang/StringBuilder object when you use the plus (+) concatenation operator.
It is more efficient to explicitly use java/lang/StringBuilder to build a string than to use the +
concatenation operator.
Figure 4, Figure 5 and Figure 6 show an example of this.
In Java, strings are immutable. Therefore, in Figure 4Error! Reference source not found., the
character x is not appended to the string that str points to. The concatenation + operator implicitly
creates a StringBuilder object and copies the string (which str points to) into this new object. It then
appends the character x to the contents of the new object.
public class myClass {
public static int N = 10000;
public static void main(String [] argv) {
String str = “”;
for (int i = 0; i < N; i++)
str = str + “x”;
}
}
Figure 4. Example of avoiding unnecessary allocations: New objects implicitly created inside loop on every iteration
Figure 5 shows the code that javac generates.
public class myClass {
public static int N = 10000;
public static void main(String [] argv) {
String str = “”;
for (int i = 0; i < N; i++)
StringBuilder sb = new StringBuilder();
sb.append(str);
sb.append(“x”);
str = sb.toString();
}
}
Figure 5. Example of avoiding unnecessary allocations: Code generated by javac for loop in Figure 3a
IBM Just-In-Time Compiler (JIT) for Java
Best practices and coding guidelines for improving performance
Dostları ilə paylaş: |