Code Monkey home page Code Monkey logo

jvm-tail-recursion's Introduction

jvm-tail-recursion

Build status Latest version

Java library performing tail recursion optimizations on Java bytecode. It simply replaces the final recursive method calls in a function to a goto to the start of the same function.

The project uses ASM to perform bytecode manipulation.

Examples

Count down to zero

A simple tail recursive function that counts down to zero:

Before After
static void count(int n) {

    if (n == 0) {
        return;
    }
    count(n - 1);
    
}
static void count(int n) {
    while (true) {
        if (n == 0) {
            return;
        }
        n = n - 1;
    }
}

List numbers in a string

If you've ever wanted a sequence of numbers in a string:

Before After
static String numbers(int n, String s) {

    if (n == 0) {
        return s + "0";
    }
    return numbers(n - 1, s + n + ",");
    
    
}
static String numbers(int n, String s) {
    while (true) {
        if (n == 0) {
            return s + "0";
        }
        s = s + n + ",";
        n = n - 1;
    }
}

Enumerate all interfaces that a class implements

Tail recursive class can also be optimized inside an if-else condition. Note that here only the recursive call at the end is optimized, not the one in the loop!

Before After
static void collectInterfaces(
        Class<?> clazz, 
        Set<Class<?>> result) {
        
    for (Class<?> itf : clazz.getInterfaces()) {
        if (result.add(itf)) 
            collectInterfaces(itf, result);
    }
    Class<?> sclass = clazz.getSuperclass();
    if (sclass != null) {
        collectInterfaces(sclass, result);
        
    }
    
    
}
static void collectInterfaces(
        Class<?> clazz, 
        Set<Class<?>> result) {
    while (true) {
        for (Class<?> itf : clazz.getInterfaces()) {
            if (result.add(itf)) 
                collectInterfaces(itf, result);
        }
        Class<?> sclass = clazz.getSuperclass();
        if (sclass != null) {
            clazz = sclass;
            continue;
        }
        return;
    }
}

Side effect free instruction removal

If there are some instructions in the return path of a tail recursive call which can be removed, the library will do so. Some of these are:

  • Unused variables
  • Unused allocated arrays
  • Unused field or array accesses
Before After
final void count(int n) {

    if (n == 0) {
        return;
    }
    count(n - 1);
    Object b = new int[Integer.MAX_INT];
    Object f = this.myField;
    Object a = this.myArray[30];
}
final void count(int n) {
    while (true) {
        if (n == 0) {
            return;
        }
        n = n - 1;
    }

    
}

Note that this causes some exceptions to not be thrown in case of programming errors. E.g. No OutOfMemoryError will be thrown in the optimized code as the new int[Integer.MAX_INT] instruction is optimized out, and no NullPointerExceptions are thrown if the myArray field is null.

this instance change

The optimization can be performed even if the tail recursive call is done on a different instance: (See limitations)

Before After
public class MyClass {
    public final void count(int n) {

        if (n == 0) {
            return;
        }
        new MyClass().count(n - 1);
        
        
    }
}
public class MyClass {
    public final void count(int n) {
        while (true) {
            if (n == 0) {
                return;
            }
            this = new MyClass();
            n = n - 1;
        }
    }
}

Note that setting the this variable is not valid Java code, but it is possible in bytecode.

Limitations

There are some limitations to the optimization:

  1. The method must not be virtual. A virtual method is called based on the dynamic type of the object. This means that instance methods which are virtual cannot be optimized, as if the object on which the method is being invoked on changes, the recursive call cannot be simplified to a jump.
    • Another reason is why virtual methods cannot be optimized is that they can be called from the subclass using the super.method(...) expression. If a subclass calls the method then the tail recursive calls would need to be dispatched back to the subclass. This causes virtual methods to not be optimizable.
  2. Synchronized instance methods cannot be optimized. See the previous point for the reasons.
    • static method can be synchronized.
  3. If you throw an exception in the method, the stacktrace will only show the method once, as the tail recursive calls are optimized.

Recommendations

The methods you want to be subject to optimization should be any of the following:

  • static method.
  • private instance method.
  • final instance method.

Usage

The project is released as the sipka.jvm.tailrec package on the saker.nest repository.
You can download the latest release using this link or by selecting a version and clicking Download on the Bundles tab on the sipka.jvm.tailrec package page.

It can be used in the following ways:

Part of the build process

The project integrates with the saker.build system in the following ways:

ZIP transformer

Using the sipka.jvm.tailrec.zip.transformer() task to retrieve a ZIP transformer when creating your JAR or ZIP archive will cause each .class file to be optimized by the library.

$javac = saker.java.compile(src)
saker.jar.create(
    Resources: {
        Directory: $javac[ClassDirectory],
        Resources: **,
    },
    Transformers: sipka.jvm.tailrec.zip.transformer(),
)

The above is an example for compiling all Java sources in the src directory, and creating a JAR file with the compiled and optimized classes.

Class directory optimization

You can use the sipka.jvm.tailrec.optimize() task to optimize a directory with the .class files in it.

$javac = saker.java.compile(src)

$path = sipka.jvm.tailrec.optimize($javac[ClassDirectory])

The above will simply optimize all the .class files that are the output of the Java compilation. The optimized classes are written into the build directory, and a path to it is returned by the task. ($path)

Optimize an existing archive

If you already have an archive that you want to optimize, use the ZIP transformer as seen previously, but specify the inputs as your archive:

saker.jar.create(
    Include: my_jar_to_optimize.jar,
    Transformers: sipka.jvm.tailrec.zip.transformer(),
)

This will result in a new archive being created that contains everything from the included JAR, and each .class file will be optimized.

Command line usage

The optimization can also be performed on the command line:

java -jar sipka.jvm.tailrec.jar -output my_jar_opt.jar my_jar.jar

The above will optimize my_jar.jar and create the output of my_jar_opt.jar. You can also overwrite the input:

java -jar sipka.jvm.tailrec.jar -overwrite my_jar.jar

Which causes the input JAR to be overwritten with the result.

The input can also be a class directory.

See --help for more usage information.

With saker.build

If you already have the saker.build system at hand, you don't have to bother with downloading. You can use the main action of saker.nest to invoke the library:

java -jar saker.build.jar action main sipka.jvm.tailrec --help

Benchmarks

Our results are the following: (Higher values are better)

See the benchmark directory for more information.

Optimized

Benchmark                            Mode  Cnt        Score      Error  Units
TailRecursionBenchmark.countTest    thrpt   25   436354,616 ? 2208,882  ops/s
TailRecursionBenchmark.factTest     thrpt   25  1201126,490 ? 8081,594  ops/s
TailRecursionBenchmark.numbersTest  thrpt   25     2183,977 ?   62,684  ops/s

Unoptimized

Benchmark                            Mode  Cnt        Score      Error  Units
TailRecursionBenchmark.countTest    thrpt   25   257429,802 ? 1501,296  ops/s
TailRecursionBenchmark.factTest     thrpt   25   831008,693 ? 9108,785  ops/s
TailRecursionBenchmark.numbersTest  thrpt   25     2083,716 ?   14,563  ops/s

Building the project

The project uses the saker.build system for building.

Use the following command, or do build it inside an IDE:

java -jar saker.build.jar -build-directory build export

See the build script for the executable build targets.

Repository structure

  • src: The source files for the project
    • Sources for the ASM library are under the package sipka.jvm.tailrec.thirdparty.
  • resources: Resource files for the created JAR files
  • test/src: Test Java sources
  • test/resources: Resource files for test cases which need them

Should I use this?

You should use it, but you should not rely on it.
In general, when you're writing production code, you'll most likely already optimize your methods in ways that it already avoids issues that are solvable with tail recursion optimization.

My recommendation is that in general you shouldn't rely on a specific optimization being performed for you. They are subject to the circumstances, and can easily break without the intention of breaking it. For example, by not paying attention and accidentally adding a new instruction after the tail recursive call that you want to optimize, will cause the optimization to not be performed. This could cause your code to break unexpectedly.
If you want an optimization done for you, you should do it yourself, or be extremely explicit about it. Make sure to add tests for the scenarios that you want to work in a specific way.

This project serves mainly educational purposes, and is also fun as you can write infinite loops like this:

public static void loopForever() {
    System.out.println("Hello world!");
    
    loopForever();
}

Magnificent!

License

The source code for the project is licensed under GNU General Public License v3.0 only.

Short identifier: GPL-3.0-only.

Official releases of the project (and parts of it) may be licensed under different terms. See the particular releases for more information.

jvm-tail-recursion's People

Contributors

sipkab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

zeta1999

jvm-tail-recursion's Issues

Optimize recursive calls when unoptimizable branches are present in the return path

There may be cases when a tail recursive call has multiple execution paths after the call itself. The optimization should be performed in cases where it is possible.

One example is such:

public static int count(int n) {
	if (n == 0) {
		return 0;
	}
	int v = count(n - 1);
	if (n == 0) {
		callSomeOtherMethod();
	}
	return v;
}

Should be optimized to:

public static int count(int n) {
start:
	if (n == 0) {
		return 0;
	}
	
	if (n == 0) {
		int v = count(n - 1);
		callSomeOtherMethod();
		return v;
	}
	n = n - 1;
	goto start;
}

Current state

Currently we allow branches to be present in the return path, but they all need to be side-effect free. This issue requires individual modifications and analysis to the subsequent execution paths.

Benchmark

Are there any benchmarks showing the effect of applying the tail-recursion optimizations?

ASM error: Class versions V1_5 or less must use F_NEW frames.

I'm getting this error:

Exception in thread "main" java.lang.IllegalArgumentException: Class versions V1_5 or less must use F_NEW frames.
        at sipka.jvm.tailrec.thirdparty.org.objectweb.asm.MethodWriter.visitFrame(MethodWriter.java:779)
        at sipka.jvm.tailrec.thirdparty.org.objectweb.asm.tree.FrameNode.accept(FrameNode.java:141)
        at sipka.jvm.tailrec.thirdparty.org.objectweb.asm.tree.InsnList.accept(InsnList.java:145)
        at sipka.jvm.tailrec.thirdparty.org.objectweb.asm.tree.MethodNode.accept(MethodNode.java:752)
        at sipka.jvm.tailrec.thirdparty.org.objectweb.asm.tree.MethodNode.accept(MethodNode.java:650)
        at sipka.jvm.tailrec.thirdparty.org.objectweb.asm.tree.ClassNode.accept(ClassNode.java:466)
        at sipka.jvm.tailrec.TailRecursionOptimizer.optimizeMethods(TailRecursionOptimizer.java:114)
        at sipka.jvm.tailrec.TailRecursionOptimizer.optimizeMethods(TailRecursionOptimizer.java:92)
        at sipka.jvm.tailrec.MainCommand.optimizeJar(MainCommand.java:208)
        at sipka.jvm.tailrec.MainCommand.optimizeFile(MainCommand.java:182)
        at sipka.jvm.tailrec.MainCommand.call(MainCommand.java:139)
        at sipka.jvm.tailrec.Main.lambda$parse$1(Main.java:166)
        at sipka.jvm.tailrec.Main.callCommand(Main.java:195)
        at sipka.jvm.tailrec.Main.main(Main.java:177)

Version: v0.8.1
Java version used to run: Java 15

The target JAR uses Java 8 as far as I know, but it has some libraries that should be backwards compatible or such for my understanding, otherwise who would on the earth still uses Java 5? There is Java 15!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.