Quali eventuali compilatori C ++ eseguono l'ottimizzazione della ricorsione della coda?


150

Mi sembra che funzionerebbe perfettamente per l'ottimizzazione della ricorsione della coda sia in C che in C ++, ma durante il debug non sembra mai vedere uno stack di frame che indica questa ottimizzazione. Questo è abbastanza buono, perché lo stack mi dice quanto è profonda la ricorsione. Tuttavia, anche l'ottimizzazione sarebbe piacevole.

Alcuni compilatori C ++ eseguono questa ottimizzazione? Perché? Perchè no?

Come faccio a dire al compilatore di farlo?

  • Per MSVC: /O2o/Ox
  • Per GCC: -O2o-O3

Che ne dici di verificare se il compilatore lo ha fatto in un determinato caso?

  • Per MSVC, abilitare l'output PDB per poter tracciare il codice, quindi ispezionare il codice
  • Per GCC ..?

Prenderò comunque suggerimenti su come determinare se una determinata funzione è ottimizzata in questo modo dal compilatore (anche se trovo rassicurante che Konrad mi dica di assumerlo)

È sempre possibile verificare se il compilatore lo fa eseguendo una ricorsione infinita e verificando se si traduce in un loop infinito o in uno stack overflow (l'ho fatto con GCC e ho scoperto che -O2è sufficiente), ma voglio essere in grado di controllare una determinata funzione che so terminerà comunque. Mi piacerebbe avere un modo semplice per controllare questo :)


Dopo alcuni test, ho scoperto che i distruttori rovinano la possibilità di effettuare questa ottimizzazione. A volte può valere la pena cambiare l'ambito di determinate variabili e temporali per assicurarsi che escano dall'ambito prima dell'inizio dell'istruzione return.

Se è necessario eseguire un distruttore dopo la coda, non è possibile eseguire l'ottimizzazione della coda.

Risposte:


129

Tutti gli attuali compilatori mainstream eseguono abbastanza bene l' ottimizzazione delle chiamate di coda (e lo fanno da più di un decennio), anche per chiamate reciprocamente ricorsive come:

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

Consentire al compilatore di eseguire l'ottimizzazione è semplice: basta attivare l'ottimizzazione per la velocità:

  • Per MSVC, utilizzare /O2o /Ox.
  • Per GCC, Clang e ICC, utilizzare -O3

Un modo semplice per verificare se il compilatore ha eseguito l'ottimizzazione è quello di eseguire una chiamata che altrimenti comporterebbe un overflow dello stack o guardare l'output dell'assembly.

Come interessante nota storica, l'ottimizzazione del call tail per C è stata aggiunta al GCC nel corso di una tesi di diploma di Mark Probst. La tesi descrive alcune avvertenze interessanti nell'implementazione. Vale la pena leggere.


L'ICC lo farebbe, credo. Per quanto ne so, ICC produce il codice più veloce sul mercato.
Paul Nathan,

35
@Paul La domanda è quanto della velocità del codice ICC sia causata da ottimizzazioni algoritmiche come le ottimizzazioni delle chiamate di coda e quanto sia causata dalle ottimizzazioni della cache e della microistruzione che solo Intel, con la sua profonda conoscenza dei propri processori, può fare.
Imagist,

6
gccha un'opzione più ristretta -foptimize-sibling-callsper "ottimizzare le chiamate ricorsive di pari livello". Questa opzione (in base alle gcc(1)pagine di manuale per le versioni 4.4, 4.7 e 4.8 mira varie piattaforme) è abilitato a livelli -O2, -O3, -Os.
FooF,

Inoltre, l'esecuzione in modalità DEBUG senza richiedere esplicitamente ottimizzazioni NON farà NESSUNA ottimizzazione. È possibile abilitare PDB per la vera modalità di rilascio EXE e provare a superarla, ma si noti che il debug in modalità di rilascio ha le sue complicazioni: variabili invisibili / rimosse, variabili unite, variabili che escono dall'ambito in un ambito sconosciuto / imprevisto, variabili che non entrano mai ambito e divenne una costante costante con indirizzi a livello di stack e - beh - frame stack uniti o mancanti. Di solito i frame stack uniti significano che la call center è inline e che i frame mancanti / backmerger probabilmente chiamano la coda.
Петър Петров

21

gcc 4.3.2 incorpora completamente questa funzione ( atoi()implementazione scadente / banale ) in main(). Il livello di ottimizzazione è -O1. Noto se ci gioco (anche cambiando da statica extern, la ricorsione della coda scompare abbastanza velocemente, quindi non dipenderei da esso per la correttezza del programma.

#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
Tuttavia, puoi attivare l'ottimizzazione del tempo di collegamento e immagino che anche un externmetodo potrebbe essere incorporato allora.
Konrad Rudolph,

5
Strano. Ho appena testato gcc 4.2.3 (x86, Slackware 12.1) e gcc 4.6.2 (AMD64, Debian wheezy) e con-O1 non c'è allineamento e ottimizzazione della ricorsione della coda . Devi usarlo -O2per questo (beh, in 4.2.x, che è piuttosto antico ora, non sarà ancora integrato). A proposito, vale anche la pena aggiungere che gcc può ottimizzare la ricorsione anche quando non è strettamente una coda (come fattoriale senza accumulatore).
przemoc,

16

Oltre all'ovvio (i compilatori non eseguono questo tipo di ottimizzazione a meno che non lo richiediate), esiste una complessità nell'ottimizzazione delle chiamate in coda in C ++: destructors.

Dato qualcosa di simile:

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

Il compilatore non può (in generale) tail-call ottimizzare questo perché deve chiamare il distruttore di cls dopo che la chiamata ricorsiva ritorna.

A volte il compilatore può vedere che il distruttore non ha effetti collaterali visibili esternamente (quindi può essere fatto in anticipo), ma spesso non può.

Una forma particolarmente comune di questo è dove Funkyè effettivamente un std::vectoro simile.


Non funziona per me. Il sistema mi dice che il mio voto è bloccato fino a quando la risposta non viene modificata.
hmuelner,

Ho appena modificato la risposta (rimosse le parentesi) e ora ho potuto annullare il mio voto negativo.
hmuelner,

11

La maggior parte dei compilatori non esegue alcun tipo di ottimizzazione in una build di debug.

Se usi VC, prova a creare una versione di rilascio con le informazioni PDB attivate: questo ti consentirà di rintracciare l'app ottimizzata e, si spera, dovresti vedere ciò che desideri allora. Nota, tuttavia, che il debug e il tracciamento di una build ottimizzata ti salteranno dappertutto e spesso non puoi ispezionare direttamente le variabili poiché finiscono sempre nei registri o vengono completamente ottimizzate. È un'esperienza "interessante" ...


2
prova gcc perché -g -O3 e per ottenere opimizzazioni in una build di debug. xlC ha lo stesso comportamento.
g24l,

Quando dici "la maggior parte dei compilatori": quali raccolte di compilatori consideri? Come sottolineato, ci sono almeno due compilatori che eseguono ottimizzazioni durante la compilazione del debug - e per quanto ne so lo fa anche VC (tranne se si sta abilitando la modifica e continua forse).
skyking

7

Come menziona Greg, i compilatori non lo faranno in modalità debug. Va bene che le build di debug siano più lente di quelle di un prod, ma non dovrebbero andare in crash più spesso: e se dipendi da un'ottimizzazione delle chiamate di coda, potrebbero fare esattamente questo. Per questo motivo è spesso meglio riscrivere la chiamata di coda come un normale ciclo. :-(

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.