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

Я хотел проверить, поддерживает ли g++ tail calling, поэтому я написал эту простую программу, чтобы проверить ее: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 в обоих из них, пока хвост называем версия нужна еще одна int. (Почему?)
Поэтому я подумал, что оптимизация может быть полезной:

> 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++, которому нужен стек, поэтому я попробовал 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++ не делает оптимизацию хвостового вызова, в то время как она явно доступна на платформе? есть ли способ заставить его?

3 ответов


потому что вы передаете временный объект 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);
}

Compile with: g++- O2-fno-exceptions-o tailcall tailcall.cpp

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

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

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


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

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

анализ жизни и похоже, это делается слишком консервативно. Установка глобального указателя для ссылки на локальный отключает TCO, даже если время жизни локального (область) заканчивается до хвостового вызова.
{
    int x;
    static int * p;
    p = & x;
} // x is dead here, but the enclosing function still has TCO disabled.

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

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

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


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

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

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

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

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

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

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

аналогичная проблема наблюдается в Pass-by-reference препятствует GCC от устранения хвостового вызова

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