Weighing in on Java native compilation | ![]() | ![]() ![]() ![]() ![]() ![]() | ![]() | |
![]() | ||||
![]() |
![]() |
The pros and cons of generating native code from Java source
Martyn Honeyford
(martynh@uk.ibm.com) When it was first introduced, it seemed that Java native compilation would surely topple the JVM, taking with it the Java platform's hard-fought platform independence. But even with its growing popularity and the increasing number of native compilers on the market, native compilation has a way to go before it poses a real threat to Java code's portability. Unfortunately, it also may be a while before the technology is mature enough to resolve the Java performance issues so many of us struggle with today. Share your thoughts on this article with the author and other readers in the discussion forum by clicking Discuss at the top or bottom of the article. Despite its many high points, there are still several issues with the Java language that rule out its use in key projects. These include execution speed, memory footprint, disk footprint, and JVM availability. JIT compilers do much to improve the platform's execution speed and J2ME greatly reduces its memory footprint, but in many domains Java applications simply cannot compete with their native (typically C/C++) counterparts. To resolve these problems, many developers have turned to Java native compilers, which allow applications to be written in the Java language and then compiled into native executables. This solution will cost you in terms of platform independence, but it can result in the faster execution and smaller footprint essential to so many of today's applications. To bring you up to speed on Java native compilation technology, we'll start with a discussion of the basics of code compilation, including a brief overview of why many developers are employing Java native compilers for their applications. Next, we'll test the results of Java native compilation, using a free software compiler and two different applications (one very simple, the other more sophisticated). These examples and the resulting metrics will serve as a first-hand look at how the recent Java native compilers compare with the JVM. Code compilation basics
Compiling Java code using a Java compiler is straightforward. We simply write the source code in the Java language, use a Java compiler to compile the source into Java bytecode, and execute the results on any hardware/OS platform that has a JVM installed. Java's reliance on the JVM for its signature "write once, run anywhere" portability is its downside; not only must a JVM be available for any platform on which you want to run your Java apps, but there must be significant system resources (memory and disk space) to support that JVM. As a result, many developers continue to rely on less flexible but more targeted languages such as C/C++. Compiling source in C/C++ is similar to doing so in Java. Once the code is written, we run it through a compiler and linker targeted to a specific hardware/OS platform. The resulting application will be executable only on the targeted platform, but will not require that a JVM be installed (though it may require some supporting shared libraries, depending on language used). All but the most simple applications developed using this method must be tailored individually to each hardware/OS platform on which you want them to run. The third method attempts to bridge the best of each of the above solutions, allowing developers to write applications in the Java language and compile them into native executables. Once the Java code is written, it can be run through a Java compiler to produce Java bytecode, which is then compiled into native code, or it can be run directly into a Java native compiler. The number of steps involved depends on the requirements of the compiler used. The advantage of this approach is that the resulting code can be executed on the targeted platform without the JVM. This is intended to result in Java applications that execute at much improved speeds and require significantly less disk space and memory to run (though it may be necessary to provide supporting libraries for the Java native compiler). Compilers vary in the platforms they target, the level of Java support they provide, and the amount of system resources they use. You'll find a listing of some of the currently available native compilers in this article's Resources section. About the test setup My test system hardware consists of a PC with a Pentium II processor running at 450 MHz and containing 320 MB of memory. The OS is a recent install of the Mandrake 8.1 Linux distribution. This distribution comes with version 3.0.1 of GCJ, which is included in GCC 3.0.1 and ships as part of the 8.1 Mandrake distribution. I've run two separate applications, one very simple and one that is more complex. To compare the system's performance against that of the Java platform, I compiled the applications into Java bytecode. I compiled the Java code using the Sun JDK version 1.3.1.02 for Linux, then tested the resulting class on the following JVMs:
For the purpose of this article, I've measured execution speed, execution memory overhead, and disk space. Test 1: Prime.java
As you can see, the code loops from 0 to 50000. As it goes, it attempts to divide each number it encounters by every number up to itself, to find out if there is a remainder. (This is, admittedly, the brute-force method of ferreting out primes, but it will suffice for the example.) I compiled prime.java into a native executable with the command:
The argument To compile the Java bytecode test, I used the command:
Next, I invoked the code for each of our test JVMs, using the following commands:
Test results for prime.java Table 1. Prime.java: Execution speed
Table 2. Prime.java: Memory usage
Note that the VM size equals the total size of the image of the process. This includes all code, data, and shared libraries used by the process, including pages that have been swapped out. VM resident set size (RSS) is equal to the size of the part of the process (code and data) that actually resides in RAM, including shared libraries. This gives a fair approximation of how much RAM a process is using. In simple terms, if a process allocates a large amount of memory it will show up in the VM size, but it won't show up in VM RSS until it is actually being used (for example, read or written). VM RSS is actually the more important measure, because it gives a greater indication of the performance hit on the system. Table 3. Prime.java: Disk space usage
Note that the measurements shown in Table 3 exclude shared libraries and the JVM, and are measured with the executable stripped. Test 2: SciMark 2 I used the following command to compile SciMark 2 into a native executable:
And I used this command to compile the application into Java bytecode:
The SciMark 2 benchmark can be run in two modes, normal and large. The mode you use determines the size of the problem sets used. I've run the tests in both modes. To invoke the code in normal mode, I used the following commands:
For the larger problem sets I used these commands:
Test results for SciMark 2 Table 4. SciMark 2, mode normal: Execution speed
Table 5. SciMark 2, mode normal: Memory usage
Table 6. SciMark 2, mode large: Execution speed
Table 7. SciMark 2, mode large: Memory usage
Table 8. SciMark 2: Disk space usage for both modes
Once again, the measurements in Table 8 exclude shared libraries and the JVM, and are measured with the executable stripped. Pros and cons of native compilation The native version is a clear winner over the JVM version only when it comes to disk space, and this is true only when the size of the JVM is taken into account. While the classes themselves are very small, the JVMs tested were huge (a recursive directory listing in the jre subdirectory of both the IBM and Sun JVMs shows that the JREs alone take up over 50 MB of disk space). But bear in mind that there are much smaller JVMs available and, while the combination of JVM and a single application was much larger than that of a native executable and the GCJ runtime library, libgcj.so (which is under 3 MB), the executable size for the native version was much larger. Thus, in situations where a large number of applications are required, the JVM version may ultimately be the winner. In addition to these somewhat nebulous results, a number of potential problems can arise from the use of Java native compilers. They are as follows:
Conclusion
Despite the relative immaturity of the technology and the lack of clear-cut results, Java native compilation is an exciting new area for the Java language. The best way to take advantage of the existing options is to research and test them yourself, perhaps using some of the methods and criteria established in this article. While native compilation isn't the JVM killer that many people thought it would be, it has proven to be just the right solution for some applications and environments. Native compilation extends the use of Java language into domains where it simply wasn't applicable just a few short years ago. This can only be a good thing for the Java language and for the Java community as a whole.
|