Может ли рекурсивная функция быть встроенной?

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

Читая это, я обнаружил, что приведенный выше код приведет к «бесконечной компиляции», если компилятор неправильно обработает его.

Как компилятор решает, встроить функцию или нет?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
138
0
42 380
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

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

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

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

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

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

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

Ответ принят как подходящий

Во-первых, спецификация функции 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).

это #pragma inline_recursion (on). Документация о максимальной глубине не является последовательной или неубедительной. Возможны значения 8, 16 или значение #pragma inline_depth.

peterchen 26.10.2008 04:11

@peterchen Если встроенная функция меняет значение одного из своих аргументов, что происходит, я думаю, что лучше встроить функцию в fact вместо main. Извините за мой английский

ob_dev 24.12.2011 11:14

@obounaim: Вы могли так подумать. MSVC этого не делает.

SecurityMatt 11.04.2013 10:36

«Как компилятор решает, встроить функцию или нет?»

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

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

Утверждение Википедии о том, что рекурсивные макросы обычно являются незаконными, выглядит недостаточно информированным. C и C++ предотвращают рекурсивные вызовы, но единица трансляции не становится незаконной, если она содержит код макроса, который выглядит так, как будто он был бы рекурсивным. В ассемблерах рекурсивные макросы обычно разрешены.

Посмотрите уже предоставленные ответы о том, почему это обычно не работает.

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

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

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

Это очень мило, но обратите внимание, что исходное сообщение имело переменный аргумент "int n".

Windows programmer 10.10.2008 09:54

Верно, но также мало смысла в желании «рекурсивного встраивания», когда n неизвестно во время компиляции ... как компилятор мог этого добиться? Так что в контексте вопроса я думаю, что это уместная альтернатива.

yungchin 10.10.2008 16:00

См. Пример Дерека Парка, как это сделать: дважды встраиваясь, вы выполняете рекурсию n >> 2 раз и получаете 2 + 2 возврата из результирующего кода.

MSalters 10.10.2008 18:55

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

Некоторые рекурсивные функции можно преобразовать в циклы, которые фактически бесконечно встраивают их. Я считаю, что gcc может это сделать, но я не знаю о других компиляторах.

Некоторые компиляторы (например, Borland C++) не имеют встроенного кода, содержащего условные операторы (if, case, while и т. д.), Поэтому рекурсивная функция в вашем примере не будет встроена.

Другие вопросы по теме