Une fonction récursive peut-elle être en ligne?

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

Pendant que je lisais ceci , j'ai trouvé que le code ci-dessus conduirait à une "compilation infinie" s'il n'était pas géré correctement par le compilateur.

Comment le compilateur décide-t-il d'insérer une fonction ou non?

Ashwin
la source

Réponses:

137

Premièrement, la inlinespécification d'une fonction n'est qu'un indice. Le compilateur peut (et le fait souvent) ignorer complètement la présence ou l'absence d'un inlinequalificatif. Cela dit, un compilateur peut insérer une fonction récursive, tout comme il peut dérouler une boucle infinie. Il doit simplement placer une limite sur le niveau auquel il "déroulera" la fonction.

Un compilateur d'optimisation peut transformer ce code:

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

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

dans ce code:

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);
            }
        }
    }
}

Dans ce cas, nous avons fondamentalement intégré la fonction 3 fois. Certains compilateurs font effectuer cette optimisation. Je me souviens que MSVC ++ avait un paramètre pour régler le niveau d'inlining qui serait effectué sur les fonctions récursives (jusqu'à 20, je crois).

Parc Derek
la source
20
c'est #pragma inline_recursion (on). La documentation sur la profondeur maximale n'est ni cohérente ni concluante. Les valeurs 8, 16 ou la valeur de #pragma inline_depth sont possibles.
peterchen
@peterchen Si la fonction inlined change la valeur d'un de ses arguments ce qui se passe, je pense qu'il vaut mieux intégrer la fonction inside fact plutôt que main. Désolé pour mon anglais
ob_dev
1
@obounaim: Vous pourriez penser cela. MSVC ne le fait pas.
SecurityMatt
23

En effet, si votre compilateur n'agit pas intelligemment, il peut essayer d'insérer des copies de votre inlinefonction d de manière récursive, créant du code infiniment grand. La plupart des compilateurs modernes le reconnaîtront cependant. Ils peuvent soit:

  1. Pas du tout en ligne la fonction
  2. Inlinez-le jusqu'à une certaine profondeur, et s'il ne s'est pas terminé d'ici là, appelez l'instance distincte de votre fonction en utilisant la convention d'appel de fonction standard. Cela peut prendre en charge de nombreux cas courants de manière haute performance, tout en laissant une solution de secours pour le cas rare avec une grande profondeur d'appel. Cela signifie également que vous conservez les versions intégrées et séparées du code de cette fonction.

Pour le cas 2, de nombreux compilateurs ont des #pragmas que vous pouvez définir pour spécifier la profondeur maximale à laquelle cela doit être fait. Dans gcc , vous pouvez également le transmettre à partir de la ligne de commande avec --max-inline-insns-recursive(voir plus d'informations ici ).

Matt J
la source
7

AFAIK GCC fera si possible l'élimination des appels de queue sur les fonctions récursives. Votre fonction n'est cependant pas récursive en queue.

leppie
la source
6

Le compilateur crée un graphe d'appel; lorsqu'un cycle est détecté en s'appelant, la fonction n'est plus en ligne après une certaine profondeur (n = 1, 10, 100, quel que soit le réglage du compilateur).

Paul Nathan
la source
3

Certaines fonctions récursives peuvent être transformées en boucles, qui les insèrent effectivement à l'infini. Je crois que gcc peut le faire, mais je ne connais pas les autres compilateurs.

Alex étrange
la source
2

Voir les réponses déjà données pour savoir pourquoi cela ne fonctionne généralement pas.

En tant que "note de bas de page", vous pouvez obtenir l'effet que vous recherchez (au moins pour la factorielle que vous utilisez comme exemple) en utilisant une métaprogrammation de modèle . Collage depuis Wikipedia:

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

template <>
struct Factorial<0> 
{
    enum { value = 1 };
};
Yungchin
la source
1
C'est très mignon, mais veuillez noter que la publication d'origine avait un argument variable "int n".
Programmeur Windows
1
C'est vrai, mais il n'y a pas non plus de raison de vouloir "l'inlining récursif" quand n n'est pas connu au moment de la compilation ... comment le compilateur pourrait-il y parvenir? Donc, dans le contexte de la question, je pense que c'est une alternative pertinente.
yungchin
1
Voir l'exemple de Derek Park pour savoir comment procéder: en insérant deux fois, vous répétez n >> 2 fois et vous obtenez 2 + 2 retours à partir du code résultant.
MSalters
1

Le compilateur créera un graphe d'appel pour détecter ce genre de choses et les empêcher. Ainsi, il verrait que la fonction s'appelle elle-même et non en ligne.

Mais principalement, il est contrôlé par le mot-clé en ligne et les commutateurs du compilateur (par exemple, vous pouvez l'avoir automatiquement en ligne de petites fonctions même sans le mot-clé.) Il est important de noter que les compilations de débogage ne doivent jamais être intégrées car la pile d'appels ne sera pas conservée pour refléter les appels que vous avez créés dans le code.

mattlant
la source
1

"Comment le compilateur décide-t-il s'il doit incorporer une fonction ou non?"

Cela dépend du compilateur, des options spécifiées, du numéro de version du compilateur, peut-être de la quantité de mémoire disponible, etc.

Le code source du programme doit toujours obéir aux règles des fonctions intégrées. Que la fonction soit intégrée ou non, vous devez vous préparer à la possibilité qu'elle soit intégrée (un nombre inconnu de fois).

La déclaration de Wikipedia selon laquelle les macros récursives sont généralement illégales semble plutôt mal informée. C et C ++ empêchent les invocations récursives mais une unité de traduction ne devient pas illégale en contenant du code de macro qui semble avoir été récursif. Dans les assembleurs, les macros récursives sont généralement légales.

Programmeur Windows
la source
0

Certains compilateurs (Ie Borland C ++) n'intègrent pas de code contenant des instructions conditionnelles (if, case, while etc.) donc la fonction récursive de votre exemple ne serait pas insérée.

Roger Nelson
la source