Які, якщо такі, компілятори C ++ роблять оптимізацію хвостової рекурсії?


150

Мені здається, що було б ідеально справно робити оптимізацію хвостової рекурсії як в C, так і в C ++, але під час налагодження я ніколи не бачу фреймового стека, який вказує на цю оптимізацію. Це добре, бо стек підказує мені, наскільки глибока рекурсія. Однак оптимізація також була б приємною.

Чи роблять цю оптимізацію будь-які компілятори C ++? Чому? Чому ні?

Як мені сказати компілятору зробити це?

  • Для MSVC: /O2або/Ox
  • Для GCC: -O2або-O3

Як щодо перевірки, чи компілятор це зробив у певному випадку?

  • Для MSVC увімкніть вихід PDB, щоб він міг відстежувати код, а потім перевірити код
  • Для GCC ..?

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

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


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

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

Відповіді:


129

Усі поточні компілятори мейнстріму досить добре виконують оптимізацію хвостових викликів (і роблять це вже більше десяти років), навіть для взаємно рекурсивних викликів, таких як:

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 під час дипломної роботи Марка Пробста. У дипломній роботі описані деякі цікаві застереження щодо реалізації. Варто прочитати.


ICC зробив би це так, я вважаю. Наскільки мені відомо, ICC виробляє найшвидший код на ринку.
Пол Натан

35
@Paul Питання полягає в тому, яка швидкість коду ICC обумовлена ​​алгоритмічними оптимізаціями, такими як оптимізація хвостових викликів і скільки викликана оптимізаціями кешу та мікроінструкції, які може зробити лише Intel, маючи глибоке знання власних процесорів.
Imagist

6
gccє більш вузький варіант -foptimize-sibling-callsдля "оптимізації повторних та зворотних дзвінків". Ця опція (по gcc(1)сторінкам довідника версій 4.4, 4.7 і 4.8 , орієнтований на різні платформи) включена на рівні -O2, -O3, -Os.
FooF

Крім того, запуск у режимі DEBUG без явного запиту на оптимізацію НЕ БУДЕ НЕ БУДЬ ЛЮБОЮ оптимізацію. Ви можете ввімкнути PDB в режимі справжнього випуску EXE і спробувати перейти через це, але зауважте, що налагодження в режимі випуску має свої ускладнення - невидимі / викреслені змінні, об'єднані змінні, змінні, що виходять із сфери застосування у невідомому / несподіваному обсязі, змінні ніколи не заходять сфери та стали справжніми константами з адресами на рівні стека, і - добре - об'єднаними або відсутніми кадрами стека. Зазвичай рамки злитого стека означають, що виклик є накресленим, а кадри, які відсутні / занурені, мабуть, викликають хвіст.
Петър Петров

21

gcc 4.3.2 повністю вказує цю функцію (хитра / тривіальна 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;
}

1
Ви можете активувати оптимізацію часу зв’язку, і я думаю, що тоді навіть externметод може бути накреслений.
Конрад Рудольф

5
Дивно. Я просто перевіряв GCC 4.2.3 (x86, Slackware 12.1) і GCC 4.6.2 (AMD64, Debian свистячих) і з-O1 немає ні вбудовування та немає оптимізації хвостовій рекурсії . Ви повинні використовувати -O2для цього (ну, в 4.2.x, який досить давній зараз, він все ще не буде накреслений). BTW Також варто додати, що gcc може оптимізувати рекурсію навіть тоді, коли вона не є суворою хвостовою (як, наприклад, факторний без акумулятор).
przemoc

16

Як і очевидне (компілятори не роблять подібного роду оптимізацію, якщо ви цього не вимагаєте), в деструкторах C ++: є складність щодо оптимізації хвостових викликів.

Дано щось на кшталт:

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

Компілятор не може (в цілому) хвостовим викликом оптимізувати це, оскільки йому потрібно викликати деструктор cls після повернення рекурсивного виклику.

Іноді компілятор може побачити, що у деструктора немає зовнішніх видимих ​​побічних ефектів (тому це можна зробити рано), але часто він не може.

Особливо поширеною формою цього є те, де Funkyнасправді є std::vectorабо подібне.


Не працює для мене. Система каже мені, що мій голос заблокований, поки відповідь не буде відредагована.
hmuelner

Щойно відредагував відповідь (вилучив парантези), і тепер я міг скасувати свою анкету.
hmuelner

11

Більшість компіляторів не роблять оптимізації для складання налагодження.

Якщо ви використовуєте VC, спробуйте збірку версій із увімкненою інформацією про PDB - це дозволить простежити за оптимізованим додатком, і ви, сподіваємось, побачите, що ви хочете тоді. Однак зауважте, що налагодження та відстеження оптимізованої збірки буде стрибати вас всюди, і часто ви не можете перевіряти змінні безпосередньо, оскільки вони лише коли-небудь потрапляють у регістри або отримують оптимізацію повністю. Це "цікавий" досвід ...


2
спробуйте gcc чому -g -O3 і отримати оптимізацію у налагодженні. xlC має таку саму поведінку.
g24l

Коли ви говорите "більшість компіляторів": які колекції компіляторів ви вважаєте? Як було зазначено, є щонайменше два компілятори, які здійснюють оптимізацію під час збирання налагодження - і наскільки я знаю, VC робить це теж (за винятком випадків, коли ви дозволяєте змінювати і продовжувати, можливо).
злетіння

7

Як зазначає Грег, компілятори не будуть робити це в режимі налагодження. Це нормально, що збірки налагоджень будуть повільнішими, ніж збірки prod, але вони не повинні виходити з ладу частіше: і якщо ви залежите від оптимізації хвостових викликів, вони можуть зробити саме це. Через це часто краще переписати хвостовий дзвінок як звичайний цикл. :-(

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.