Оптимизация хвостового вызова для функции фибоначчи в java

Я изучал рекурсию Tail call и наткнулся на некоторые упомянутые документы. Sun Java не реализует оптимизацию хвостовых вызовов. Я написал следующий код для вычисления числа фибоначчи тремя разными способами: 1. Итеративный 2. Рекурсивная головка 3. Рекурсивный хвост

public class Fibonacci {
    public static void main(String[] args) throws InterruptedException {
        int n = Integer.parseInt(args[0]);
        System.out.println("\n Value of n : " + n);
        System.out.println("\n Using Iteration : ");
        long l1 = System.nanoTime();
        fibonacciIterative(n);
        long l2 = System.nanoTime();
        System.out.println("iterative time = " + (l2 - l1));
        System.out.println(fibonacciIterative(n));

        System.out.println("\n Using Tail recursion : ");
        long l3 = System.nanoTime();
        fibonacciTail(n);
        long l4 = System.nanoTime();
        System.out.println("Tail recursive time = " + (l4 - l3));
        System.out.println(fibonacciTail(n));

        System.out.println("\n Using Recursion : ");
        long l5 = System.nanoTime();
        fibonacciRecursive(n);
        long l6 = System.nanoTime();
        System.out.println("Head recursive time = " + (l6 - l5));
    }

    private static long fibonacciRecursive(int num) {
        if (num == 0) {
            return 0L;
        }
        if (num == 1) {
            return 1L;
        }
        return fibonacciRecursive(num - 1) + fibonacciRecursive(num - 2);
    }

    private static long fibonacciIterative(int n) throws InterruptedException {
        long[] arr = new long[n + 1];
        arr[0] = 0;
        arr[1] = 1;
        for (int i = 2; i <= n; i++) {
            // Thread.sleep(1);
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[n];
    }

    private static long fibonacciTail(int n) {
        if (n == 0)
            return 0;
        return fibHelper(n, 1, 0, 1);
    }

    private static long fibHelper(int n, int m, long fibM_minus_one, long fibM) {
        if (n == m)
            return fibM;
        return fibHelper(n, m + 1, fibM, fibM_minus_one + fibM);
    }
}

При запуске этой программы я получил некоторые результаты:

  • Головной рекурсивный метод не заканчивается для n > 50. Программа выглядела как повешенная. Любая идея, почему это может произойти?
  • Рекурсивный метод хвоста занимал значительно меньше времени по сравнению с рекурсией головы. Иногда требуется меньшее время, чем Iterative method. Означает ли это, что java делает внутреннюю оптимизацию Tail? И если это так, то почему я это сделал, это дает StackOverflowError при n > 5000?

Системные спецификации:

Процессор Intel Core 5,

Windows XP,

32-разрядная версия Java 1.6

Размер стека по умолчанию для JVM.

Ответ 1

Означает ли это, что java делает внутреннюю оптимизацию вызовов Tail?

Нет, это не так. Компиляторы HotSpot JIT не реализуют оптимизацию хвостового вызова.

Результаты, которые вы наблюдаете, типичны для аномалий, которые вы видите в тесте Java, который не учитывает разминку JVM. Например, вызывается "первые несколько" раз, когда метод вызывается интерпретатором. Тогда компилятор JIT скомпилирует метод... и он будет быстрее.

Чтобы получить значимые результаты, поместите цикл вокруг всей партии и запустите ее несколько раз, пока тайминги не стабилизируются. Затем отбросьте результаты от ранних итераций.

... почему я это сделал, давая StackOverflowError при n > 5000?

Это просто доказательство того, что оптимизация "хвоста" не происходит.

Ответ 2

Для первого вопроса, что такое 2 ^ 50 (или что-то близкое)? Каждое число N в рекурсивной функции Fib вызывает его дважды (до 2). Каждый из этих вызовов 2 предшествует итерациям и т.д., Поэтому он растет до 2 ^ (N-k) рекурсии (k, вероятно, 2 или 3).

Второй вопрос заключается в том, что второй - прямая N рекурсия. Вместо того, чтобы идти двуглавым (N-1),(N-2), он просто строится из M = 1, M = 2... M = N. Каждый шаг пути, значение N-1 сохраняется для добавления. Поскольку это операция O (N), она сопоставима с итерационным методом, с той лишь разницей, что компилятор JIT оптимизирует ее. Однако проблема с рекурсией заключается в том, что для каждого уровня, который вы складываете на фрейм, требуется огромный объем памяти, у вас на некоторое время заканчивается память или пространство стека. Он все равно должен быть медленнее, чем итеративный метод.

Ответ 3

Вы можете использовать Memoization, чтобы избежать рекурсии головы.

Я тестировал следующий код, когда N <= 40, этот подход является плохим, поскольку Map имеет компромисс.

private static final Map<Integer,Long> map = new HashMap<Integer,Long>();

private static long fibonacciRecursiveMemoization(int num) {
    if (num == 0) {
        return 0L;
    }
    if (num == 1) {
        return 1L;
    }

    int num1 = num - 1;
    int num2 = num - 2;

    long numResult1 = 0;
    long numResult2 = 0;

    if(map.containsKey(num1)){
        numResult1 = map.get(num1);
    }else{
        numResult1 = fibonacciRecursiveMemoization(num1);
        map.put(num1, numResult1);
    }

    if(map.containsKey(num2)){
        numResult2 = map.get(num2);
    }else{
        numResult2 = fibonacciRecursiveMemoization(num2);
        map.put(num2, numResult2);
    }

    return numResult1 + numResult2;
}

когда значение n: 44

Использование итерации: итерационное время = 6984

Использование рекурсии хвоста: Рекурсивное время хвоста = 8940

Использование Memoization Recursion: Рекурсивное время воспоминания = 1799949

Использование рекурсии: Начальное рекурсивное время = 12697568825

Ответ 4

Относительно точки 1: Вычисление чисел Фибоначчи рекурсивно без memoization приводит к экспоненциальному времени выполнения в n. Это относится к любому языку программирования, который автоматически не запоминает результаты функции (например, большинство основных нефункциональных языков, например Java, С#, С++,...). Причина в том, что одни и те же функции будут вызываться снова и снова - например. f(8) вызовет f(7) и f(6); f(7) вызовет f(6) и f(5), так что f(6) вызывается дважды. Этот эффект распространяется и вызывает экспоненциальный рост числа вызовов функций. Здесь вызывается визуализация того, какие функции вызываются:

f(8)
 f(7)
  f(6)
   f(5)
    f(4)
     ...
    f(3)
     ...
   f(4)
    ...
  f(5)
   f(4)
    ...
   f(3)
    ...
 f(6)
  f(5)
   ...
  f(4)
   ...