Чи може рекурсивна функція бути вбудованою?


134
inline int factorial(int n)
{
    if(!n) return 1;
    else return n*factorial(n-1);
}

Коли я читав це , виявив, що наведений вище код призведе до "нескінченної компіляції", якщо компілятор не обробляється правильно.

Як компілятор вирішує, вбудувати функцію чи ні?

Відповіді:


137

По-перше, inlineспецифікація функції - лише натяк. Компілятор може (і часто це робить) повністю ігнорувати наявність або відсутність inlineкваліфікатора. З урахуванням сказаного компілятор може вбудовувати рекурсивну функцію стільки, скільки може розкрутити нескінченний цикл. Він просто повинен встановити обмеження на рівень, до якого він буде "розкручувати" функцію.

Оптимізуючий компілятор може перетворити цей код:

inline int factorial(int n)
{
    if (n <= 1)
    {
        return 1;
    }
    else
    {
        return n * factorial(n - 1);
    }
}

int f(int x)
{
    return factorial(x);
}

в цей код:

int factorial(int n)
{
    if (n <= 1)
    {
        return 1;
    }
    else
    {
        return n * factorial(n - 1);
    }
}

int f(int x)
{
    if (x <= 1)
    {
        return 1;
    }
    else
    {
        int x2 = x - 1;
        if (x2 <= 1)
        {
            return x * 1;
        }
        else
        {
            int x3 = x2 - 1;
            if (x3 <= 1)
            {
                return x * x2 * 1;
            }
            else
            {
                return x * x2 * x3 * factorial(x3 - 1);
            }
        }
    }
}

У цьому випадку ми в основному впорядкували функцію 3 рази. Деякі компілятори роблять виконати цю оптимізацію. Я пригадую, що MSVC ++ має налаштування для настройки рівня вбудовування, який би виконувався на рекурсивних функціях (я вважаю, до 20).


20
це #pragma inline_recursion (on). Документація про максимальну глибину не є послідовною чи непереконливою. Можливі значення 8, 16 або значення #pragma inline_depth.
peterchen

@peterchen Якщо вбудована функція змінює значення одного з її аргументів, що відбувається, я думаю, що краще вбудувати функцію всередині факту замість основного. Вибачте за мою англійську
ob_dev

1
@obounaim: Ви можете подумати про це. MSVC не робить.
SecurityMatt

23

Дійсно, якщо ваш компілятор не діє розумно, він може спробувати вставити копії inlineфункції d рекурсивно, створюючи нескінченно великий код. Однак, більшість сучасних компіляторів визнають це. Вони можуть:

  1. Не вбудована функція взагалі
  2. Вкладіть його на певну глибину, і якщо вона до цього часу не закінчилася, зателефонуйте до окремого екземпляра вашої функції, використовуючи стандартну функцію виклику функції. Це може забезпечити високу ефективність багатьох випадків, залишаючи при цьому резерв для рідкісних випадків з великою глибиною виклику. Це також означає, що ви зберігаєте як вбудовані, так і окремі версії коду цієї функції навколо.

У випадку 2, у багатьох компіляторів #pragmaви можете встановити, щоб вказати максимальну глибину, на якій це слід зробити. У gcc ви також можете передати це з командного рядка --max-inline-insns-recursive(див. Докладнішу інформацію тут ).


7

Якщо можливо, AFAIK GCC виконує усунення хвостових викликів на рекурсивних функціях. Однак ваша функція не є рекурсивною.


6

Компілятор створює графік викликів; при виявленні циклу, що викликає саму себе, функція після певної глибини більше не вбудовується (n = 1, 10, 100, незалежно від налаштування компілятора).


3

Деякі рекурсивні функції можуть бути перетворені в петлі, що ефективно нескінченно їх окреслює. Я вважаю, що gcc може це зробити, але я не знаю про інші компілятори.


2

Перегляньте вже наведені відповіді, чому це зазвичай не працює.

Як "виноска", ви можете досягти ефекту, який ви шукаєте (принаймні для фактора, яку ви використовуєте як приклад), використовуючи метапрограмування шаблонів . Вставка з Вікіпедії:

template <int N>
struct Factorial 
{
    enum { value = N * Factorial<N - 1>::value };
};

template <>
struct Factorial<0> 
{
    enum { value = 1 };
};

1
Це дуже мило, але зауважте, що в оригінальній публікації був аргумент змінної "int n".
програміст Windows

1
Щоправда, але також мало сенсу бажати "рекурсивного вбудовування", коли n невідомо під час компіляції ... як компілятор міг цього досягти? Тож у контексті питання я думаю, що це відповідна альтернатива.
юнчін

1
Дивіться приклад Дерека Парка, як це зробити: Вклавши два рази, ви повторюєте n >> 2 рази, і ви отримуєте 2 + 2 повернення з отриманого коду.
MSalters

1

Компілятор зробить графік викликів, щоб виявити такі речі та запобігти їх. Так було б видно, що функція викликає себе, а не вбудована.

Але в основному це контролюється вбудованими ключовими словами та компіляторами (наприклад, ви можете мати автоматичні вбудовані невеликі функції навіть без ключового слова.) Важливо відзначити, що компіляції налагодження ніколи не повинні бути вкладеними, оскільки стовп виклику не буде збережений для дзеркала дзвінки, створені вами в коді.


1

"Як компілятор вирішує, вбудувати функцію чи ні?"

Це залежить від компілятора, параметрів, які були вказані, номера версії компілятора, можливо, скільки пам’яті доступно тощо.

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

Заява Вікіпедії про те, що рекурсивні макроси, як правило, є незаконними, виглядає досить погано. C і C ++ запобігають рекурсивним викликам, але блок перекладу не стає незаконним, містять макрокод, який виглядає так, як це було б рекурсивно. У асемблерах рекурсивні макроси, як правило, є законними.


0

Деякі компілятори (тобто Borland C ++) не містять вбудований код, який містить умовні висловлювання (якщо, випадок, тоді як і т. Д.), Тому рекурсивна функція у вашому прикладі не була б окреслена.

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