Una funzione ricorsiva può essere in linea?


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

Mentre stavo leggendo questo , ho scoperto che il codice sopra avrebbe portato a "compilazione infinita" se non gestito correttamente dal compilatore.

In che modo il compilatore decide se incorporare una funzione o no?

Risposte:


137

Innanzitutto, la inlinespecifica di una funzione è solo un suggerimento. Il compilatore può (e spesso lo fa) ignorare completamente la presenza o l'assenza di un inlinequalificatore. Detto questo, un compilatore può incorporare una funzione ricorsiva, così come può srotolare un ciclo infinito. Deve semplicemente porre un limite al livello in cui "srotolerà" la funzione.

Un compilatore di ottimizzazione potrebbe trasformare questo codice:

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

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

in questo codice:

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

In questo caso, abbiamo sostanzialmente integrato la funzione 3 volte. Alcuni compilatori fanno eseguire questa ottimizzazione. Ricordo che MSVC ++ aveva un'impostazione per sintonizzare il livello di inline che verrebbe eseguito su funzioni ricorsive (fino a 20, credo).


20
è #pragma inline_recursion (attivo). La documentazione sulla profondità massima non è coerente o inconcludente. Sono possibili i valori 8, 16 o il valore di #pragma inline_depth.
peterchen,

@peterchen Se la funzione inline sta cambiando il valore di uno dei suoi argomenti, cosa succede, penso che sia meglio incorporare la funzione all'interno dei fatti anziché in main.
Ci

1
@obounaim: potresti pensarlo. MSVC no.
SecurityMatt

23

In effetti, se il compilatore non agisce in modo intelligente, potrebbe provare a inserire inlinericorsivamente copie della funzione d, creando un codice infinitamente grande. La maggior parte dei compilatori moderni lo riconoscerà, tuttavia. Possono:

  1. Non incorporare affatto la funzione
  2. Inline fino a una certa profondità, e se non è terminato da allora, chiama l'istanza separata della tua funzione usando la convenzione di chiamata della funzione standard. Questo può occuparsi di molti casi comuni in modo ad alte prestazioni, lasciando un fallback per il raro caso con una profondità di chiamata elevata. Questo significa anche che mantieni in circolazione sia versioni separate che separate del codice di quella funzione.

Per il caso 2, molti compilatori #pragmapossono essere impostati per specificare la profondità massima a cui ciò dovrebbe essere fatto. In gcc , puoi anche passarlo dalla riga di comando con --max-inline-insns-recursive(vedi maggiori informazioni qui ).


7

AFAIK GCC eseguirà l'eliminazione delle chiamate di coda sulle funzioni ricorsive, se possibile. La tua funzione tuttavia non è ricorsiva alla coda.


6

Il compilatore crea un grafico di chiamata; quando viene rilevato un ciclo che chiama se stesso, la funzione non viene più evidenziata dopo una certa profondità (n = 1, 10, 100, qualunque sia il sintonizzatore su cui è sintonizzato il compilatore).


3

Alcune funzioni ricorsive possono essere trasformate in loop, che effettivamente li allinea all'infinito. Credo che gcc possa farlo, ma non conosco altri compilatori.


2

Vedi le risposte già fornite per capire perché di solito non funziona.

Come "nota a piè di pagina", potresti ottenere l'effetto che stai cercando (almeno per il fattoriale che stai usando come esempio) usando la metaprogrammazione del modello . Incollando da Wikipedia:

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

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

1
È molto carino, ma tieni presente che la pubblicazione originale aveva un argomento variabile "int n".
Programmatore Windows

1
Vero, ma c'è anche poco senso nel voler "inline ricorsive" quando n non è noto al momento della compilazione ... come potrebbe mai il compilatore riuscirci? Quindi, nel contesto della domanda, penso che questa sia un'alternativa rilevante.
yungchin,

1
Vedi l'esempio di Derek Park su come farlo: Inline due volte, si contano n >> 2 volte e si ottengono 2 + 2 ritorni dal codice risultante.
MSalters,

1

Il compilatore creerà un grafico di chiamata per rilevare questo tipo di cose e prevenirle. Quindi vedrebbe che la funzione si chiama se stessa e non in linea.

Ma principalmente è controllato dalle parole chiave inline e dagli switch del compilatore (Ad esempio, puoi avere auto inline piccole funzioni anche senza la parola chiave.) È importante notare che le compilazioni di debug non dovrebbero mai essere in linea poiché il callstack non verrà conservato per rispecchiarsi le chiamate che hai creato nel codice.


1

"In che modo il compilatore decide se incorporare una funzione o no?"

Dipende dal compilatore, dalle opzioni specificate, dal numero di versione del compilatore, forse da quanta memoria è disponibile, ecc.

Il codice sorgente del programma deve ancora obbedire alle regole per le funzioni incorporate. Indipendentemente dal fatto che la funzione venga incorporata, è necessario prepararsi alla possibilità che venga incorporata (un numero sconosciuto di volte).

L'affermazione di Wikipedia secondo cui le macro ricorsive sono in genere illegali sembra piuttosto scarsamente informata. C e C ++ impediscono invocazioni ricorsive ma un'unità di traduzione non diventa illegale contenendo un codice macro che sembra ricorsivo. Negli assemblatori, le macro ricorsive sono in genere legali.


0

Alcuni compilatori (ovvero Borland C ++) non incorporano il codice che contiene istruzioni condizionali (if, case, while etc ..), quindi la funzione ricorsiva nel tuo esempio non verrebbe incorporata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.