Почему это кодирует исчерпанный память в Java, но не в c?

Или в Java или в c я могу записать функцию как

fun(){
  fun();
}

(игнорирование деталей синтаксиса)

В Java я получаю исключение OutOfMemory, но в C (и возможно некоторые другие языки) это, кажется, работает навсегда, как будто это был бесконечный цикл. Почему я не получаю ошибку OutOfMemory здесь также?

9
задан dmckee 18 February 2010 в 18:57
поделиться

8 ответов

Поскольку ваша функция является примером хвостовой рекурсии , то, скорее всего, компилятор C оптимизирует рекурсию до итерации, заставляя ее зацикливаться бесконечно без сбоев.

19
ответ дан 4 December 2019 в 06:08
поделиться

Другие ответчики правы, что есть некоторая магия компилятора, которая преобразует хвостовую рекурсию в итерацию, хотя это зависит от оптимизации настройки компилятора. Например, в gcc, если мы скомпилируем с помощью gcc -S -O1 someFile.c (учитывая ваш код), мы получим следующую сгенерированную сборку:

fun:
.LFB2:
        pushq   %rbp
.LCFI0:
        movq    %rsp, %rbp
.LCFI1:
        movl    $0, %eax
        call    fun
        leave
        ret
.LFE2:
        .size   fun, .-fun

Итак, вы можете видеть, он все еще использует call / leave / ret инструкции для выполнения фактического вызова функции, которая убьет процесс. Как только вы начнете оптимизацию с помощью gcc -S -O2 someFile.c , мы начнем получать волшебство:

fun:
.LFB24:
        .p2align 4,,10
        .p2align 3
.L2:
        jmp     .L2
.LFE24:    
        .size   fun, .-fun
        .p2align 4,,15

Это зависит от вашего компилятора и настроек компилятора, поэтому дружить с ними помогает.

12
ответ дан 4 December 2019 в 06:08
поделиться

Причина в том, что компилятор C, вероятно, рассматривает это как хвостовой рекурсивный вызов и, следовательно, избегает построения стека для выполнения функции. Поскольку стек не создается для этого вызова, он превращается из рекурсии в простой бесконечный цикл. Вы можете заставить ее создавать стек, сделав ее головной рекурсивной

int fun() { 1 + fun(); }
9
ответ дан 4 December 2019 в 06:08
поделиться

Как отмечали другие, это оптимизация рекурсии хвостового вызова, выполненная компилятором языка Си. Как всегда, помогает конкретный пример. Без включенных оптимизаций gcc (v3.4.6) выдает следующий ассемблерный код x86:-

fun:
    pushl   %ebp
    movl    %esp, %ebp
    call    fun
    leave
    ret
    .size   fun, .-fun

Обратите внимание на рекурсивный вызов fun(). Если он будет выполнен, то в конце концов переполнится стек и произойдет крах, но при -O2 gcc выдает:-

    fun:
        pushl   %ebp
        movl    %esp, %ebp
.L2:
        jmp     .L2
        .size   fun, .-fun

Обратите внимание на бесконечный цикл без инструкции возврата? Он просто будет выполняться вечно.

4
ответ дан 4 December 2019 в 06:08
поделиться

В C это можно было бы оптимизировать как хвостовой рекурсивный вызов. Таким образом, вызов fun () на самом деле не вызывает себя; он просто перезапускает функцию (как goto). Другими словами, компилятор рассматривает его так, как если бы он был написан следующим образом:

void fun() {
start:
    goto start;
}

Итак, стек не будет увеличиваться.

2
ответ дан 4 December 2019 в 06:08
поделиться

Если реализация языка программирования имеет оптимизацию хвостовых вызовов, то она скомпилирует эту рекурсию в цикл. Текущая Java VM не имеет оптимизации хвостовых вызовов, поэтому это закончится java.lang.StackOverflowError.

Возможно, когда-нибудь в будущем Java VM будет иметь оптимизацию хвостовых вызовов, потому что функциональные языки программирования, которые работают на JVM (Scala, Clojure и т.д.), выиграют от этого (сейчас, по крайней мере, компилятор Scala делает свою собственную оптимизацию хвостовых вызовов для прямой рекурсии, но, AFAIK, не для косвенной рекурсии).

2
ответ дан 4 December 2019 в 06:08
поделиться

Ваш компилятор c, вероятно, использует хвостовую рекурсию. Каждый раз, когда вы входите в новую функцию, компьютер добавляет запись в стек. Эта запись указывает, куда ЦП должен вернуться после выполнения процедуры. Теперь в случае, приведенном выше, поскольку вызов fun () внутри fun () является последним вызовом в функции, компилятор c может оптимизировать выталкивание стека и вместо этого создавать хвостовой вызов. Фактически вы можете использовать это для создания цикла:

int foo(int from, int to)
{
    if (from == to) return from;
    dosomething();
    from ++;
    foo(from, to);
}

Многие языки (например, Erlang) вообще не имеют циклов и вместо этого используют описанный выше метод для создания циклов.

Java не поддерживает хвостовую рекурсию.

1
ответ дан 4 December 2019 в 06:08
поделиться

Вы получите аномальное завершение программы через некоторое время. Ваш код содержит неопределенную рекурсию, и каждый вызов fun() помещает в стек дополнительные байты. В зависимости от размера вашей памяти и ваших ограничений, приложение завершится после, по крайней мере, 500 вызовов. Это может занять некоторое время, но вы получите исключительное завершение.

ВМ java ограничивает глубину рекурсии до некоторого уровня, поэтому она завершится в ближайшее время.

-4
ответ дан 4 December 2019 в 06:08
поделиться
Другие вопросы по тегам:

Похожие вопросы: