Какие, если есть, компиляторы С++ выполняют оптимизацию хвостовой рекурсии?

Мне кажется, что будет отлично работать для оптимизации хвостовых рекурсий как в C, так и в C++, но во время отладки я никогда не вижу стека кадров, который указывает на эту оптимизацию. Это хорошо, потому что стек говорит мне, насколько глубока рекурсия. Однако оптимизация была бы неплохой.

Выполняют ли какие-либо C++ компиляторы? Зачем? Почему бы и нет?

Как мне заставить компилятор сделать это?

  • Для MSVC: /O2 или /Ox
  • Для GCC: -O2 или -O3

Как насчет проверки, если компилятор сделал это в определенном случае?

  • Для MSVC включите вывод PDB для отслеживания кода, затем проверьте код
  • Для GCC..?

Я по-прежнему принимаю предложения о том, как определить, оптимизирована ли какая-то функция компилятором (хотя я считаю, что это успокаивает то, что Конрад говорит мне предположить)

Всегда можно проверить, делает ли это компилятор вообще, делая бесконечную рекурсию и проверяя, приводит ли она к бесконечному циклу или переполнению стека (я сделал это с GCC и обнаружил, что -O2 достаточно), но я хочу, чтобы иметь возможность проверить определенную функцию, которую я знаю, все равно прекратится. Я хотел бы иметь простой способ проверить это :)


После некоторого тестирования я обнаружил, что деструкторы разрушают возможность этой оптимизации. Иногда бывает полезно изменить область видимости определенных переменных и временных рядов, чтобы убедиться, что они выходят из области действия до начала операции return-statement.

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

Ответ 1

Все текущие компиляторы основного потока довольно хорошо выполняют оптимизацию хвостовых вызовов (и выполняются уже более десяти лет) даже для взаимно-рекурсивных вызовов, таких как:

int bar(int, int);

int foo(int n, int acc) {
    return (n == 0) ? acc : bar(n - 1, acc + 2);
}

int bar(int n, int acc) {
    return (n == 0) ? acc : foo(n - 1, acc + 1);
}

Предоставление компилятору оптимизации просто: просто включите оптимизацию для скорости:

  • Для MSVC используйте /O2 или /Ox.
  • Для GCC, Clang и ICC используйте -O3

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

Как интересная историческая заметка, оптимизация хвостового вызова для C была добавлена в GCC в ходе дипломной работы Марка Пробста. В диссертации описываются некоторые интересные предостережения в реализации. Это стоит прочитать.

Ответ 2

gcc 4.3.2 полностью встраивает эту функцию (реалистичность (crappy/trivial atoi())) в main(). Уровень оптимизации -O1. Я замечаю, что если я поиграю с ним (даже изменив его с static на extern, хвостовая рекурсия уходит довольно быстро, поэтому я не буду зависеть от нее для правильности программы.

#include <stdio.h>
static int atoi(const char *str, int n)
{
    if (str == 0 || *str == 0)
        return n;
    return atoi(str+1, n*10 + *str-'0');
}
int main(int argc, char **argv)
{
    for (int i = 1; i != argc; ++i)
        printf("%s -> %d\n", argv[i], atoi(argv[i], 0));
    return 0;
}

Ответ 3

Большинство компиляторов не делают никакой оптимизации в отладочной сборке.

Если вы используете VC, попробуйте создать версию релиза с включенной информацией PDB - это позволит вам отслеживать оптимизированное приложение, и вы, надеюсь, будете видеть, что вы хотите. Обратите внимание, однако, что отладка и трассировка оптимизированной сборки будут вам повсюду перемещаться, и часто вы не можете напрямую проверять переменные, поскольку они только когда-либо заканчиваются в регистрах или полностью оптимизируются. Это "интересный" опыт...

Ответ 4

Как и очевидное (компиляторы не выполняют такую ​​оптимизацию, если вы не просите об этом), существует сложность оптимизации хвостов в С++: деструкторы.

Учитывая что-то вроде:

   int fn(int j, int i)
   {
      if (i <= 0) return j;
      Funky cls(j,i);
      return fn(j, i-1);
   }

Компилятор не может (в общем) хвост-вызов оптимизировать это, потому что ему нужно для вызова деструктора cls после возврата рекурсивного вызова.

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

Наиболее распространенной формой этого является то, что Funky на самом деле a std::vector или подобное.

Ответ 5

Как говорит Грег, компиляторы не будут делать это в режиме отладки. Это нормально для отладочных построений, которые будут медленнее, чем сборка prod, но они не должны вылетать чаще: и если вы зависите от оптимизации хвостового вызова, они могут сделать именно это. Из-за этого часто лучше всего переписать хвостовой вызов как обычный цикл.: - (