Почему оптимизация хвоста g++ не выполняется, а gcc?

Я хотел проверить, поддерживает ли g++ хвост, поэтому я написал эту простую программу, чтобы проверить его: http://ideone.com/hnXHv

using namespace std;

size_t st;

void PrintStackTop(const std::string &type)
{
    int stack_top;
    if(st == 0) st = (size_t) &stack_top;
    cout << "In " << type << " call version, the stack top is: " << (st - (size_t) &stack_top) << endl;
}

int TailCallFactorial(int n, int a = 1)
{
    PrintStackTop("tail");
    if(n < 2)
        return a;
    return TailCallFactorial(n - 1, n * a);
}

int NormalCallFactorial(int n)
{
    PrintStackTop("normal");
    if(n < 2)
        return 1;
    return NormalCallFactorial(n - 1) * n;
}


int main(int argc, char *argv[])
{
    st = 0;
    cout << TailCallFactorial(5) << endl;
    st = 0;
    cout << NormalCallFactorial(5) << endl;
    return 0;
}

Когда я скомпилировал его, как обычно, g++ не замечает никакой разницы между двумя версиями:

> g++ main.cpp -o TailCall
> ./TailCall
In tail call version, the stack top is: 0
In tail call version, the stack top is: 48
In tail call version, the stack top is: 96
In tail call version, the stack top is: 144
In tail call version, the stack top is: 192
120
In normal call version, the stack top is: 0
In normal call version, the stack top is: 48
In normal call version, the stack top is: 96
In normal call version, the stack top is: 144
In normal call version, the stack top is: 192
120

Разница в стеках составляет 48 в обоих из них, тогда как для версии хвостового вызова требуется еще одна внутр. (Почему?)
Поэтому я думал, что оптимизация может быть удобной:

> g++ -O2 main.cpp -o TailCall
> ./TailCall
In tail call version, the stack top is: 0
In tail call version, the stack top is: 80
In tail call version, the stack top is: 160
In tail call version, the stack top is: 240
In tail call version, the stack top is: 320
120
In normal call version, the stack top is: 0
In normal call version, the stack top is: 64
In normal call version, the stack top is: 128
In normal call version, the stack top is: 192
In normal call version, the stack top is: 256
120

Размер стека увеличился в обоих случаях, и, хотя компилятор может думать, что мой процессор медленнее, чем моя память (что его не так), я не знаю, почему 80 байтов необходимы для простой функции. (Почему?). Такая версия хвостового вызова занимает больше места, чем обычная версия, и вполне логична, если int имеет размер 16 байт. (нет, у меня нет 128-битного процессора).
Теперь, подумав, почему компилятор не позвонил в хвост, я думал, что это могут быть исключения, потому что они сильно зависят от стека. Поэтому я пробовал без исключений:

> g++ -O2 -fno-exceptions main.cpp -o TailCall
> ./TailCall
In tail call version, the stack top is: 0
In tail call version, the stack top is: 64
In tail call version, the stack top is: 128
In tail call version, the stack top is: 192
In tail call version, the stack top is: 256
120
In normal call version, the stack top is: 0
In normal call version, the stack top is: 48
In normal call version, the stack top is: 96
In normal call version, the stack top is: 144
In normal call version, the stack top is: 192
120

Что вырезать нормальную версию обратно в неоптимизированный размер стека, а оптимизированный - по 8 байтов. еще int не 8 байтов.
Я думал, что есть что-то, что я пропустил в С++, которому нужен пакет, поэтому я попробовал c: http://ideone.com/tJPpc
Тем не менее хвост не звонит, но стек намного меньше (32 бит каждого кадра в обеих версиях). Затем я попытался с оптимизацией:

> gcc -O2 main.c -o TailCall
> ./TailCall
In tail call version, the stack top is: 0
In tail call version, the stack top is: 0
In tail call version, the stack top is: 0
In tail call version, the stack top is: 0
In tail call version, the stack top is: 0
120
In normal call version, the stack top is: 0
In normal call version, the stack top is: 0
In normal call version, the stack top is: 0
In normal call version, the stack top is: 0
In normal call version, the stack top is: 0
120

Не только его хвостовой вызов оптимизировал первый, но и хвостовой вызов оптимизировал второй!
Почему g++ не оптимизирует оптимизацию звонка, пока он явно доступен на платформе? есть ли способ заставить его?

Ответ 1

Поскольку вы передаете временный объект std::string функции PrintStackTop (std::string). Этот объект выделяется в стеке и тем самым предотвращает оптимизацию хвостового вызова.

Я изменил ваш код:

void PrintStackTopStr(char const*const type)
{
    int stack_top;
    if(st == 0) st = (size_t) &stack_top;
    cout << "In " << type << " call version, the stack top is: " << (st - (size_t) &stack_top) << endl;
}

int RealTailCallFactorial(int n, int a = 1)
{
    PrintStackTopStr("tail");
    if(n < 2)
        return a;
    return RealTailCallFactorial(n - 1, n * a);
}

Скомпилировать с помощью: g++ -O2 -fno-exceptions -o tailcall tailcall.cpp

И теперь он использует оптимизацию хвостового вызова. Вы можете увидеть это в действии, если вы используете флаг -S для сборки:

L39:
        imull   %ebx, %esi
        subl    $1, %ebx
L38:
        movl    $LC2, (%esp)
        call    __Z16PrintStackTopStrPKc
        cmpl    $1, %ebx
        jg      L39

Вы видите рекурсивный вызов в виде цикла (jg L39).

Ответ 2

Я не нашел другого ответа, удовлетворяющего, потому что локальный объект не влияет на стек после его удаления.

Вот хорошая статья, в которой упоминается, что время жизни локальных объектов распространяется на функцию, называемую хвостом. Оптимизация вызовов хвоста требует уничтожения локальных объектов перед отказом управления, GCC не будет применять его, если не будет уверен, что к локальному объекту не будет доступен хвостовой вызов.

Анализ продолжительности жизни затруднен, и похоже, что это делается слишком консервативно. Установка глобального указателя на ссылку локального отключает TCO, даже если локальное время жизни (область действия) заканчивается перед вызовом хвоста.

{
    int x;
    static int * p;
    p = & x;
} // x is dead here, but the enclosing function still has TCO disabled.

Это все еще не похоже на то, что происходит, поэтому я нашел еще одну ошибку. Передача локального значения в параметр с помощью определяемого пользователем или нетривиального деструктора также отключает TCO. (Определение деструктора = delete позволяет TCO.)

std::string имеет нетривиальный деструктор, поэтому он вызывает проблему.

Обходной путь заключается в том, чтобы делать эти вещи во вложенном вызове функции, потому что анализ времени жизни сможет сказать, что объект мертв по хвостовому вызову. Но нет необходимости отказываться от всех объектов С++.

Ответ 3

Исходный код с временным объектом std::string по-прежнему является хвостом рекурсивным, поскольку деструктор для этого объекта выполняется сразу после выхода из PrintStackTop("");, поэтому после рекурсивного оператора return ничего не должно выполняться.

Однако есть две проблемы, которые приводят к путанице оптимизации хвостовых вызовов (TCO):

  • аргумент передается ссылкой на функцию PrintStackTop
  • нетривиальный деструктор std::string

Это может быть проверено пользовательским классом, чтобы каждый из этих двух проблем мог нарушить TCO. Как отмечается в предыдущем ответе @Potatoswatter, есть обходной путь для обеих этих проблем. Достаточно обернуть вызов PrintStackTop другой функцией, чтобы помочь компилятору выполнить TCO даже с временным std::string:

void PrintStackTopTail()
{
    PrintStackTop("tail");
}
int TailCallFactorial(int n, int a = 1)
{
    PrintStackTopTail();
//...
}

Обратите внимание, что этого недостаточно, чтобы ограничить область охвата, заключая { PrintStackTop("tail"); } в фигурные скобки. Он должен быть заключен как отдельная функция.

Теперь он может быть проверен с помощью g++ версии 4.7.2 (параметры компиляции -O2), что хвостовая рекурсия заменяется циклом.

Аналогичная проблема наблюдается в Передача по ссылке препятствует gcc устранению хвостового вызова

Обратите внимание, что печать (st - (size_t) &stack_top) недостаточна, чтобы быть уверенным в том, что выполняется TCO, например, с опцией оптимизации -O3 функция TailCallFactorial выполняется автоматически в пять раз, поэтому TailCallFactorial(5) выполняется как вызов одной функции, но проблема раскрывается для больших значений аргументов (например, для TailCallFactorial(15);). Таким образом, TCO можно проверить, просмотрев вывод сборки, сгенерированный с помощью флага -S.