This article describes some issues related to local code compilation in Java. Dynamic (Real-Time) compilation or static (early) Compilation cannot meet the requirements of all Java applications. The authors compared the two compilation technologies in various execution environments and demonstrated how they complement each other.
The performance of Java applications has become a hot topic in the development community. Because the language was designed to support the portability of applications in an interpreted way, the early Java runtime provided far lower performance levels than C and C ++ compiling languages. Although these languages provide higher performance, the generated code can only be executed on a limited number of systems. Over the past decade, Java runtime vendors have developed complex dynamic compilers, often called the instant (just-in-time, JIT) compiler. When running the program, the JIT compiler selects the most frequently executed method to compile the code at the cost. The local code is compiled at runtime instead of before the program runs (the program written in C or C ++ is in the next situation), ensuring portability. Some JIT compilers can compile all the code without even using the interpreter, but these compilers still maintain the portability of Java applications by performing some operations during program execution.
Due to multiple improvements in the dynamic compilation technology, in many applications, the modern JIT compiler can generate the same performance as the static compilation of C or C ++ applications. However, many software developers still think that dynamic compilation based on experience or rumors may seriously interfere with program operations because the compiler must share the CPU with the application. Some developers strongly call for static compilation of Java code and firmly believe that this can solve performance problems. This is correct for some applications and execution environments. Static compilation can greatly improve Java performance, or it is the only practical choice. However, static compilation of Java applications brings a lot of complexity while achieving high performance. General Java developers may not fully understand the advantages of JIT dynamic compilers.
This article describes some issues related to static and dynamic compiling in Java, and focuses on the Real-time (RT) system. This section briefly describes the operating principles of the Java language interpreter program and describes the advantages and disadvantages of the modern JIT compiler for executing local code compilation. This article introduces the AOT compilation technology released by IBM in WebSphere real time and its advantages and disadvantages. Then compare the two compilation policies and point out several application domains and execution environments suitable for AOT compilation. The main point is that these two compilation technologies are not mutually exclusive: even in the various applications that use these two technologies most effectively, they also have some advantages and disadvantages that affect the application.
Execute Java program
Java programs are initially compiled in a platform-independent format (class files) at cost using the Java SDK javac program ). This format can be viewed as a Java platform because it defines all the information required to execute a Java program. The Java program execution engine, also known as the Java Runtime Environment (JRE), contains a virtual machine that implements the Java platform for a specific local platform. For example, the intel X86 platform based on Linux, the Sun Solaris platform, and the IBM system p platform running on the AIX operating system, each platform has a JRE. These JRE implement all local support, so that you can correctly execute programs written for the Java platform.
In fact, the size of the operand stack is limited, but programmers seldom write methods that exceed this limit. JVM provides security checks to notify programmers who have created such methods.
An important part of the Java platform program is the bytecode sequence, which describes the operations performed by each method in the Java class. Bytecode uses a theoretically infinitely large operand stack to describe computation. This stack-based program provides platform independence because it does not depend on the number of available registers in the CPU of any particular local platform. The definitions of operations that can be performed on the operand Stack are independent of the instruction sets of all local processors. The Java Virtual Machine (JVM) Specification defines the execution of these bytecode (see references ). When executing a Java program, any JRE used for any specific local platform must comply with the rules listed in the JVM specification.
Because there are few stack-based local platforms (the Intel x87 floating point coprocessor is an obvious exception), most local platforms cannot directly execute Java bytecode. To solve this problem, early JRE interpreted bytecode to execute Java programs. That is, the JVM repeats the operation in a loop:
◆ Obtain the next bytecode to be executed;
◆ Decoding;
◆ Obtain the required operands from the operand stack;
◆ Perform operations according to JVM specifications;
◆ Write the result back to the stack.
The advantage of this method is its simplicity: JRE developers only need to write code to process each bytecode. In addition, because the number of bytecode used to describe operations is less than 255, the implementation cost is relatively low. Of course, the disadvantage is performance: this was an early issue that caused many people to be dissatisfied with the Java platform, despite having many other advantages.
Resolving the performance gap with languages such as C or C ++ means that local code compilation for the Java platform is developed without sacrificing portability.
Compile Java code
Despite the rumors that the "one-write, run anywhere" slogan of Java programming may not be strictly set up in all circumstances, but it is true for a large number of applications. On the other hand, local compilation is essentially platform-specific. So how does the Java platform achieve local compilation performance without sacrificing platform independence? The answer is to use the JIT compiler for dynamic compilation. This method has been used for ten years (see figure 1 ):
Figure 1. JIT Compiler
When using the JIT compiler, Java programs compile in the form of one method each time, because they are executed in local processor instructions for higher performance. This process will generate an internal representation of the method, which is different from the bytecode, but its level is higher than the local command of the target processor. (The ibm jit compiler uses an Expression Tree sequence to represent methods .) The compiler executes a series of Optimizations to improve quality and efficiency. Finally, it executes a code generation step to convert the optimized internal representation to the local instruction of the target processor. The generated code depends on the runtime environment to execute some activities, such as ensuring the validity of type conversion or allocating some types of objects that cannot be directly executed in the code. The compilation thread of JIT compiler operations is separated from the application thread, so the application does not need to wait for compilation.
Figure 1 also describes the analysis framework used to observe the behavior of the execution program, and finds out the method of frequent execution by periodically sampling the thread. This framework also provides a tool for specific analysis methods to store the dynamic values that may not change during this execution of a program.
Because this JIT compilation process occurs during program execution, the platform independence can be maintained: the released Java platform code is still neutral. Languages such as C and C ++ lack this advantage because they compile locally before execution of the program and release local code to the (local platform) execution environment.
Challenges
Although the platform independence is maintained through JIT compilation, the cost is paid. Because the program is compiled during execution, the time for compiling the code is counted into the execution time of the program. Anyone who has compiled large C or C ++ programs knows that the compilation process is often slow.
To overcome this shortcoming, the modern JIT compiler uses either of the following two methods (in some cases, both methods are used at the same time ). The first method is: compile all the code, but do not execute any time-consuming analysis and conversion, so you can quickly generate code. Because the code generation speed is very fast, although you can clearly observe the overhead of compilation, this is easily concealed by the performance improvements brought about by repeated local code execution. The second method is to allocate compilation resources only to a small number of frequently executed methods (usually called Hot methods ). Low compilation overhead is more likely to be concealed by the performance advantages of repeated hot code execution. Many applications only execute a small number of hot methods, so this method effectively minimizes the compilation performance cost.
One of the major complexities of dynamic compilers is to weigh the expected benefits of understanding the compiled code so that method execution plays a significant role in the performance of the entire program. An extreme example is that after a program is executed, you know exactly which methods contribute the most to the performance of the specific execution, but compiling these methods is useless because the program has been completed. In another extreme, the method is unknown before the program is executed, but the potential benefit of each method is maximized. Most Dynamic compiler operations fall between these two extremes, and the method is a balance between the importance of understanding the expected benefits of the method.
The fact that the Java language needs to dynamically load classes has an important impact on the design of the Java compiler. What if other classes referenced by the code to be compiled have not been loaded? For example, a method needs to read the static field values of a class that has not been loaded. The Java language requires that this class be loaded and parsed to the current JVM when class reference is executed for the first time. The reference is not resolved until the first execution, which means there is no address for loading the static field from it. How does the compiler handle this possibility? The compiler generates some code for loading and parsing classes when classes are not loaded. Once the class is parsed, the original code location is modified in a thread-safe way to directly access the address of the static field, because the address is known.
The ibm jit compiler has made a lot of efforts to use secure and efficient code patching technology. Therefore, after parsing the class, the local code executed only loads the field value, just as fields have been resolved during compilation. Another method is to generate some code to check whether a field has been parsed and then load the value until the field is located. For fields that have been resolved and frequently accessed, this simple process may cause serious performance problems.
Advantages of dynamic compilation
Dynamic compilation of Java programs has some important advantages, and can even generate code better than static compilation languages, modern JIT compilers often insert hooks into the generated code to collect information about program behavior so that dynamic behavior can be better optimized if methods are selected for recompilation.
A good example of this method is to collect the length of a specific arraycopy operation. If this length remains unchanged during each operation, you can generate special code for the arraycopy length that is most frequently used, or call the code sequence that is adjusted to this length. Because of the features of the Memory System and instruction set design, the execution speed of the best general routines used to copy memory is usually slower than that used to copy code of a specific length. For example, copying 8-byte aligned data may require one or two commands to directly copy. In contrast, A general replication loop that can process any number of bytes and any alignment may require 10 commands to copy the same 8 bytes. However, even if such special code is generated for a specific length, the generated code must be copied with other lengths correctly. The code is generated only to make common operations with a longer length faster, so the average performance is improved. This kind of optimization is usually not practical for most static compilation languages, because all possible operations with a constant length in execution are much less than operations with a constant length in execution of a specific program.
Another important example of such optimization is optimization based on the class hierarchy. For example, to call a virtual method, you need to check the class call of the receiver object to find out which actual target implements the virtual method of the receiver object. Studies show that most virtual calls have only one target corresponding to all receiver objects, while the JIT compiler can generate code that is more efficient than virtual calls for direct calls. By analyzing the status of the class hierarchy after code compilation, the JIT compiler can find a target method for virtual calls and generate code that calls the target method directly instead of a slow virtual call. Of course, if the class hierarchy changes and another target method appears, the JIT compiler can correct the originally generated code to execute virtual calls. In practice, few of these corrections are required. In addition, it is very troublesome to perform such optimization statically because such correction may be required.
Dynamic compilers usually compile a small number of hot methods in a centralized manner, so they can execute more active analysis to generate better code, so that compilation returns a higher value. In fact, most modern JIT compilers also support methods that are considered hot methods. You can analyze and convert these frequently executed methods with very active optimizations common in static compilers (with little emphasis on Compilation Time) to generate better code and achieve higher performance.
The comprehensive effects of these improvements and other similar improvements are: for a large number of Java applications, dynamic compilation has made up for the gap between the static local compilation performance of languages such as C and C ++. In some cases, it even exceeds the performance of the latter.
Disadvantages
However, dynamic compilation does have some shortcomings that make it an ideal solution in some cases. For example, because it takes time to identify frequently executed methods and compile these methods, an application usually has to go through a preparation process in which the performance cannot reach its maximum value. There are several reasons for performance problems during the preparation process. First, a large amount of initial compilation may directly affect the startup time of the application. Not only does compilation delay the time when the application reaches a stable State (imagine that the Web server can perform practical and useful work only after an initial stage ), in addition, methods that are frequently executed during the preparation phase may not affect the stable performance of the application. If JIT compilation delays startup and does not significantly improve the long-term performance of the application, execution of such compilation is a waste. Although all modern JVMs execute optimization to reduce startup latency, this issue is not completely solved in all cases.
Second, some applications cannot tolerate the latency caused by dynamic compilation. Interactive applications such as GUI interfaces are like this. In this case, the compilation activity may adversely affect your use and cannot significantly improve the application performance.
Finally, applications that are used in real-time environments and have strict task time limits may be unable to tolerate the impact of compilation uncertainty on performance or the memory overhead of the dynamic compiler itself.
Therefore, although JIT compilation technology can provide a performance level equivalent to (or even better) the static language performance, dynamic compilation is not suitable for some applications. In these cases, ahead-of-time (AOT) Compilation of Java code may be a suitable solution.
AOT Java Compilation
Generally speaking, local compilation in Java should be a simple application of the compilation technology developed for traditional languages (such as C ++ or Fortran. Unfortunately, the dynamic features of the Java language bring additional complexity and affect the quality of static compilation code of Java programs. However, the basic idea is still the same: the local code of the Java method is generated before the program is executed, so that the local code can be directly used when the program is running. The goal is to avoid JIT compiler runtime performance consumption or memory consumption, or to avoid interpreting program early performance overhead.
Challenges
Dynamic class loading is a challenge facing the dynamic JIT compiler and a more important issue in AOT compilation. This class is loaded only when the code reference class is executed. Because AOT is compiled before the program is executed, the compiler cannot predict which classes are loaded. That is to say, the compiler cannot know the address of any static field, the offset of any instance field of any object, or the actual target of any call, or even the direct call (non-virtual call. When executing code, if it proves that the prediction of any such information is wrong, it means that the code is wrong and Java consistency is sacrificed.
Because the code can be executed in any environment, the class file may be different from the code compilation. For example, a JVM instance may load classes from a specific location on the disk, while the latter instance may load classes from different locations or even networks. Imagine a development environment in which bugs are being fixed: The content of class files may change with the execution of different applications. In addition, Java code may not exist before the program is executed. For example, Java reflection service usually generates new classes at runtime to Support Program behavior.
Lack of information about static, field, class, and method means that most of the functions of the Optimization Framework in the Java compiler are severely restricted. Inline may be the most important Optimization for static or dynamic compiler applications, but this optimization cannot be used because the compiler cannot learn the called target method.
Inline
Inline is a technique used to generate code at runtime to avoid overhead at the beginning and end of a program. The method is to insert the call code of a function into the function of the caller. But the biggest benefit of inline may be that the scope of code visible to the optimizer is expanded to generate higher quality code. The following is a sample code before inline:
Int Foo () {int x = 2, y = 3; return bar (x, y);} final int bar (int A, int B) {return a + B ;}
If the compiler can prove that this bar is the method called in Foo (), the code in the bar can replace the call to bar () in Foo. The bar () method is of the final type, so it must be the method called in Foo. Even in some virtual call examples, the dynamic JIT compiler can implicitly inline the code of the target method and can be used correctly in most cases. The compiler generates the following code:
Int Foo () {int x = 2, y = 3; return X + Y ;}
In this example, the optimization that simplifies the previous value propagation can generate code that returns 5 directly. If inline is not used, this optimization cannot be performed, resulting in much lower performance. If the bar () method is not parsed (for example, static compilation), this optimization cannot be executed, and the Code must execute virtual calls. During running, the other method may be called to multiply two numbers instead of the bar method. Therefore, inline cannot be used directly during the static compilation of Java programs.
AOT code must be generated without parsing every static, field, class, and method reference. During execution, each of these references must be updated using the correct values of the current runtime environment. This process may directly affect the performance of the first execution because all references will be parsed during the first execution. Of course, subsequent execution will benefit from patching code, so that instances, static fields, or method targets can be referenced more directly.
In addition, the local code generated for the Java method usually needs to use a value that is only used in a single JVM instance. For example, the Code must call some runtime routines in the JVM runtime to perform specific operations, such as searching for unresolved methods or allocating memory. The address of these runtime routines may change every time the JVM is loaded to the memory. Therefore, the AOT compilation code must be bound to the current JVM execution environment before it can be executed. Other examples include the string address and the internal position of the constant pool entry.
In WebSphere real time, AOT local code compilation is executed using jxeinajar (see figure 2. This tool applies local code compilation to all methods of all classes in the jar file, and can also selectively apply local code compilation to the required methods. The results are stored in an internal format named Java executable (jxe), but they can also be easily stored in any persistent container.
You may think that static compilation of all the code is the best method, because you can execute the maximum number of local code at runtime. However, some trade-offs can be made here. The more compilation methods, the more memory occupied by the Code. The compiled local code is about 10 times larger than the bytecode: the density of the local code itself is smaller than that of the bytecode, and the Code must contain additional metadata to bind the code to the JVM, in addition, the Code is correctly executed in case of exceptions or request stack tracing. Jar files that constitute common Java applications usually contain many methods that are rarely executed. Compiling these methods will consume memory, but there is no expected benefit. Memory consumption includes the following processes: storing code on the disk, extracting code from the disk and loading the JVM, and binding the code to the JVM. Unless code is executed multiple times, these costs cannot be compensated by the performance advantages of the local code.
Figure 2. jxeinajar
One fact that violates the issue of size is the call between the compiled method and the interpreted method (that is, the compiled method calls the interpreted method, or the opposite) it may be more costly than calling the two methods internally. Dynamic compilers reduce this overhead by eventually compiling all the interpreted Methods frequently called by JIT compilation Code. However, if dynamic compilers are not used, this overhead is inevitable. Therefore, if the method is compiled selectively, you must be cautious in the operation to minimize the conversion from the compiled method to the uncompiled method. It is very difficult to select the correct method to avoid this problem in all possible executions.
Advantages
Although AOT compilation Code has the disadvantages and challenges described above, compiling Java programs in advance can improve the performance, especially in environments where dynamic compilers cannot be used as an effective solution.
You can use AOT to compile code with caution to speed up application startup, because this code is usually much slower than JIT code, but much faster than interpreted code. In addition, because loading and binding AOT compilation Code usually takes less time than checking and dynamically compiling an important method, it can achieve that performance early in program execution. Similarly, interactive applications can quickly benefit from local code without using dynamic compilation that causes poor response capabilities.
RT applications can also get important benefits from AOT compilation code: More deterministic performance exceeds the interpreted performance. The dynamic JIT compiler used by WebSphere real time is specially adjusted for the use in the RT system. Make the compilation thread operate at a lower priority than the RT task, and make adjustments to avoid generating code with serious performance impact of uncertainty. However, in some RT environments, the JIT compiler is unacceptable. This type of environment usually requires the strictest time limit management control. In these examples, AOT compilation Code provides better original performance than interpreted code without affecting existing certainty. Eliminate JIT compilation threads or even eliminate the performance impact of thread preemption when higher-priority RT tasks are started.
Advantage and disadvantage statistics
The dynamic (JIT) compiler supports platform neutrality and generates high-quality code by leveraging the dynamic behavior of application execution and information about loaded classes and their hierarchies. However, the JIT compiler has a limited compile-time budget and affects the runtime performance of the program. On the other hand, static (AOT) compilers sacrifice platform independence and code quality because they cannot take advantage of dynamic behaviors of programs or have information about loaded classes or class hierarchies. AOT compilation has an effective and unlimited compile-time budget, because AOT Compilation Time does not affect runtime performance, but in practice developers will not wait for the completion of static compilation steps for a long time.
Table 1 summarizes the dynamic and static compiler features discussed in this article:
Table 1. comparative compilation technology
Both technologies require careful selection of compilation methods to achieve the highest performance. For a dynamic compiler, the compiler makes its own decisions, and for a static compiler, the developer makes a choice. It is hard to say whether the JIT compiler chooses the compilation method is advantageous, depending on the ability of the compiler to infer in a given situation. In most cases, we think this is an advantage.
The JIT compiler is superior in providing stable state performance because they can best optimize running programs, which is the most important in the mass production of Java systems. Static compilation can produce the best interactive performance, because there is no runtime compilation behavior to affect the user's expected response time. By adjusting the dynamic compiler, you can solve the startup and deterministic performance problems to some extent, but static compilation can provide the fastest startup speed and the highest level of certainty as needed. Table 2 compares the two compilation techniques in four different execution environments:
Table 2. optimal environment for using these technologies
Figure 3 shows the overall trend of Startup Performance and stable performance:
Figure 3. Performance Comparison Between AOT and JIT
The performance in the initial phase using the JIT compiler is low because the method must be explained first. As compilation methods increase and the time required for JIT compilation is shortened, the performance curve increases gradually and finally reaches the peak performance. On the other hand, the AOT compilation code has a much higher performance at startup than the interpretation, but it cannot achieve the highest performance that the JIT compiler can achieve. Binding static code to a JVM instance produces some overhead, so the initial performance is lower than the stable performance value, however, it can achieve stable performance faster than using the JIT compiler.
No local code compilation technology is suitable for all Java execution environments. What a technology is good at is usually the weakness of other technologies. For this reason, both compilation technologies must be used to meet the requirements of Java application developers. In fact, static and dynamic compilation can be combined to provide the maximum possible performance improvement-but it must be platform-independent. It is the main selling point of the Java language, so it is not a problem.
Conclusion
This article discusses the local code compilation in Java, mainly introduces the dynamic compilation and static AOT compilation in the form of JIT compiler, and compares the advantages and disadvantages of the two.
Although dynamic compilers have achieved great maturity over the past decade, a large number of Java applications can catch up with or exceed static compilation languages (such as C ++ or Fortran) performance. However, dynamic compilation is not suitable for some types of applications and execution environments. Although AOT compilation is known as a versatile solution to the disadvantages of dynamic compilation, due to the dynamic characteristics of the Java language itself, it also faces the challenge of providing full potential for local compilation.
These two technologies cannot meet all the requirements for local code compilation in the Java execution environment, but in turn can be used as a tool in the most effective place. These two technologies can complement each other. The proper use of the runtime systems of these two compilation models can benefit developers and users in a wide range of application development environments.
From: http://developer.51cto.com/art/200902/111902.htm