Tail recursive conversion can speed up applications, but not all JVM will do this conversion.
Many algorithms are very concise in the form of tail recursion. The compiler automatically converts this method into a loop to improve program performance. However, in the Java language specification, there is no need to perform such conversions. Therefore, not all Java virtual machines (JVMs) perform such conversions. This means that using tail recursion in the Java language may cause huge memory usage, which is not the expected result. Eric Allen explained in this article that dynamic compilation will maintain the language semantics, while static compilation usually does not. He explains why this is an important issue and provides code to help determine whether your JIT compiler performs tail recursive Code Conversion while maintaining language semantics.
Tail recursion and Its Conversion
A considerable number of packages contain loops, which occupy a large part of the total running time of the program. These loops often have to update more than one variable repeatedly, and the update of each variable often depends on the values of other variables.
If we regard iteration as a tail recursive function, we can regard these variables as function parameters. A simple reminder: If the returned value of a call is immediately returned as the value of the called function, this recursive call is tail recursion. tail recursion does not have to remember the context of the called function during the call.
Due to this feature, there is a good correspondence between the tail recursive function and the loop: each recursive call can be simply considered as a loop of multiple iterations. However, because all variable parameter values are passed to recursive calls once, the updated values can be obtained more easily in tail recursion than in loops. Furthermore, the hard-to-use break statements are often replaced by simple functions.
However, in Java programming, iteration in this way will lead to low efficiency, because a large number of recursive calls may cause the risk of stack overflow.
The solution is relatively simple: Because tail recursive functions are actually a simpler way to write loops, let the compiler automatically convert them into loops. In this way, you can take advantage of these two forms at the same time.
However, although everyone knows how to automatically convert a tail recursive function into a simple loop, this conversion is not required in Java specifications. The reason for not making such a requirement is probably that, in object-oriented languages, such conversions cannot be performed statically. On the contrary, the conversion from a tail recursive function to a simple loop must be carried out dynamically by the JIT compiler.
To understand why this is the case, consider the following failed attempt: multiply the elements in the Iterator In the Integers set.
Because the following program has an error, an exception is thrown during running. However, as demonstrated in many previous articles in this column, a program throws an exact exception (the same as a great error type identifier) it does not help to find out where the error is hidden in the program, and we do not want the compiler to change the program in this way, so that the compiled result code throws a different exception.
Listing 1. An attempt to multiply the elements in the Iterator of the Integer Set
Import java. util. Iterator;
Public class Example {
Public int product (Iterator I ){
Return productHelp (I, 0 );
}
Int productHelp (Iterator I, int accumulator ){
If (I. hasNext ()){
Return productHelp (I, accumulator * (Integer) I. next (). intValue ());
}
Else {
Return accumulator;
}
}
}
Note the error in the product method. The product method calls productHelp by assigning the accumulator to 0. The value must be 1. Otherwise, calling the product on any instance of the Example class produces a value of 0, regardless of the Iterator value.
Assume that this error has finally been corrected, but at the same time, a subclass of the Example class has been created, as shown in Listing 2:
Listing 2. Trying to capture incorrect calls like Listing 1
Import java. util .*;
Class Example {
Public int product (Iterator I ){
Return productHelp (I, 1 );
}
Int productHelp (Iterator I, int accumulator ){
If (I. hasNext ()){
Return productHelp (I, accumulator * (Integer) I. next (). intValue ());
}
Else {
Return accumulator;
}
}
}
// And, in a separate file:
Import java. util .*;
Public class Example2 extends Example {
Int productHelp (Iterator I, int accumulator ){
If (accumulator <1 ){
Throw new RuntimeException ("accumulator to productHelp must be> = 1 ");
}
Else {
Return super. productHelp (I, accumulator );
}
}
Public static void main (String [] args ){
Vertex list l = new vertex list ();
L. add (new Integer (0 ));
New Example2 (). product (l. listIterator ());
}
}
The overwritten productHelp method in Example2 tries to throw a runtime exception when the accumulator is less than "1" to capture incorrect calls to productHelp. Unfortunately, this will introduce a new error. If the Iterator contains any zero value instance, The productHelp will crash in its own recursive call.
Note that in the main method of Example2, an instance of Example2 is created and its product method is called. The program crashes because the Iterator passed to this method contains a 0 value.
However, you can see that the productHelp class Example is strictly tail recursive. Suppose a static compiler wants to convert the body of this method into a loop, as shown in listing 3:
Listing 3. An example of static compilation that does not optimize the end call
Int productHelp (Iterator I, int accumulator ){
While (I. hasNext ()){
Accumulator * = (Integer) I. next (). intValue ();
}
Return accumulator;
}
As a result, the call to productHelp became a call to the superclass method. Supermethods will calculate the results by simply looping on the iterator. No exception is thrown.
Compile this code using two different static compilers. The result is that one will throw an exception while the other will not. Think about how confusing this is.
Does your JIT perform this conversion?
Therefore, as shown in the example in listing 3, we do not expect the static compiler to execute tail recursive conversion on Java code while maintaining language semantics. On the contrary, we must rely on dynamic JIT compilation. Whether JIT will perform this conversion depends on JVM.
One way to determine whether your JIT will convert tail recursion is to compile and run the following small test classes:
Listing 4. Determine whether your JIT can be converted to tail recursion
Public class TailRecursionTest {
Private static int loop (int I ){
Return loop (I );
}
Public static void main (String [] args ){
Loop (0 );
}
}
Let's consider the loop method of this class. This method is just a recursive call to itself as long as possible. Because it will never return or affect any external variables in any way, replacing the code body as shown in listing 5 will retain the semantics of the program.
Listing 5. A dynamic conversion
Public class TailRecursionTest {
Private static int loop (int I ){
While (true ){
}
}
Public static void main (String [] args ){
Loop (0 );
}
}
In fact, this is a well-developed compiler transformation.
If your JIT compiler converts a tail recursive call to an iteration, the program runs indefinitely. It requires a small amount of memory and does not increase over time.
On the other hand, if JIT does not perform this conversion, the program will soon exhaust the stack space and report a stack overflow error.
I have twoThe results of running this program on the Java SDK are surprising. When running on SUN's Hotspot JVM (version 1.3), it is found that Hotspot does not execute this conversion. By default, the stack space is exhausted in less than one second when running on my machine.
On the other hand, the program runs on ibm jvm (version 1.3) without any problems, which indicates that ibm jvm converts code in this way.
Summary
Remember: we cannot expect that our code will always run on the JVM that will convert the tail recursive call. Therefore, to ensure that your program has the appropriate performance on all JVMs, you should always try to write the code that most naturally conforms to the tail recursion mode in the iterative style.
However, please note: as demonstrated in our example, it is easy to introduce errors when converting code in this way, whether manually or by software.