Perché .NET / C # non ottimizza per la ricorsione della chiamata di coda?


111

Ho trovato questa domanda su quali lingue ottimizzano la ricorsione della coda. Perché C # non ottimizza la ricorsione in coda, quando possibile?

Per un caso concreto, perché questo metodo non è ottimizzato in un ciclo ( Visual Studio 2008 a 32 bit, se è importante) ?:

private static void Foo(int i)
{
    if (i == 1000000)
        return;

    if (i % 100 == 0)
        Console.WriteLine(i);

    Foo(i+1);
}

Oggi stavo leggendo un libro sulle strutture dati che biforca la funzione ricorsiva in due, vale a dire preemptive(ad es. Algoritmo fattoriale) e Non-preemptive(ad es. Funzione di ackermann). L'autore ha fornito solo due esempi che ho citato senza fornire un ragionamento appropriato dietro questa biforcazione. Questa biforcazione è uguale alle funzioni ricorsive tail e non-tail?
RBT

5
Conversazione utile a riguardo di Jon skeet e Scott Hanselman su youtu.be/H2KkiRbDZyc?t=3302
Daniel B

@RBT: Penso che sia diverso. Si riferisce al numero di chiamate ricorsive. Le chiamate in coda riguardano le chiamate che appaiono in posizione di coda, ovvero l'ultima operazione eseguita da una funzione in modo che restituisca direttamente il risultato del chiamato.
JD

Risposte:


84

La compilazione JIT è un difficile bilanciamento tra non spendere troppo tempo nella fase di compilazione (rallentando così notevolmente le applicazioni di breve durata) e non fare abbastanza analisi per mantenere l'applicazione competitiva a lungo termine con una compilazione anticipata standard .

È interessante notare che i passaggi di compilazione di NGen non sono mirati ad essere più aggressivi nelle loro ottimizzazioni. Sospetto che ciò sia dovuto al fatto che semplicemente non vogliono avere bug in cui il comportamento dipende dal fatto che JIT o NGen fossero responsabili del codice macchina.

Lo stesso CLR supporta l'ottimizzazione della chiamata di coda, ma il compilatore specifico del linguaggio deve sapere come generare il codice operativo pertinente e il JIT deve essere disposto a rispettarlo. L' fsc di F # genererà i codici operativi rilevanti (sebbene per una semplice ricorsione potrebbe semplicemente convertire l'intera cosa in un whileciclo direttamente). Il csc di C # non lo fa.

Vedi questo post del blog per alcuni dettagli (molto probabilmente ora non aggiornato date le recenti modifiche JIT). Si noti che le modifiche CLR per 4.0 x86, x64 e ia64 lo rispetteranno .


2
Vedi anche questo post: social.msdn.microsoft.com/Forums/en-US/netfxtoolsdev/thread/… in cui scopro che la coda è più lenta di una normale chiamata. EEP!
zoccolo

77

Questo invio di feedback su Microsoft Connect dovrebbe rispondere alla tua domanda. Contiene una risposta ufficiale di Microsoft, quindi ti consiglio di farlo.

Grazie per il suggerimento. Abbiamo considerato l'emissione di istruzioni di chiamata di coda in diversi punti nello sviluppo del compilatore C #. Tuttavia, ci sono alcuni problemi sottili che ci hanno spinto ad evitarlo fino ad ora: 1) C'è effettivamente un costo generale non banale nell'usare l'istruzione .tail nel CLR (non è solo un'istruzione di salto come le chiamate di coda alla fine diventano in molti ambienti meno rigidi come gli ambienti di runtime di linguaggio funzionale in cui le chiamate tail sono fortemente ottimizzate). 2) Esistono pochi metodi C # reali in cui sarebbe legale emettere chiamate di coda (altri linguaggi incoraggiano modelli di codifica che hanno più ricorsione di coda, e molti che fanno molto affidamento sull'ottimizzazione delle chiamate di coda in realtà eseguono una riscrittura globale (come trasformazioni di Continuation Passing) per aumentare la quantità di ricorsione della coda). 3) In parte a causa di 2), i casi in cui i metodi C # eseguono un overflow dello stack a causa di una ricorsione profonda che avrebbe dovuto avere successo sono piuttosto rari.

Detto questo, continuiamo a guardare a questo, e in una futura versione del compilatore potremmo trovare alcuni modelli in cui ha senso emettere istruzioni .tail.

A proposito, come è stato sottolineato, vale la pena notare che la ricorsione della coda è ottimizzata su x64.


3
Potresti trovare utile anche questo: weblogs.asp.net/podwysocki/archive/2008/07/07/…
Noldorin

Nessun problema, sono contento che tu lo trovi utile.
Noldorin

17
Grazie per averlo citato, perché ora è un 404!
Roman Starkov

3
Il collegamento è ora corretto.
luksan

15

C # non ottimizza per la ricorsione della chiamata di coda perché è a questo scopo F #!

Per approfondimenti sulle condizioni che impediscono al compilatore C # di eseguire ottimizzazioni della chiamata di coda, vedere questo articolo: condizioni di chiamata di coda CLR JIT .

Interoperabilità tra C # e F #

C # e F # interagiscono molto bene e poiché .NET Common Language Runtime (CLR) è progettato tenendo presente questa interoperabilità, ogni linguaggio è progettato con ottimizzazioni specifiche per il suo intento e scopi. Per un esempio che mostra quanto sia facile chiamare codice F # da codice C #, vedere Chiamata di codice F # da codice C # ; per un esempio di chiamata di funzioni C # da codice F #, vedere chiamata di funzioni C # da F # .

Per l'interoperabilità dei delegati, vedere questo articolo: Interoperabilità dei delegati tra F #, C # e Visual Basic .

Differenze teoriche e pratiche tra C # e F #

Di seguito è riportato un articolo che copre alcune delle differenze e spiega le differenze di progettazione della ricorsione delle chiamate di coda tra C # e F #: Generazione di codice operativo della chiamata di coda in C # e F # .

Ecco un articolo con alcuni esempi in C #, F # e C ++ \ CLI: Adventures in Tail Recursion in C #, F # e C ++ \ CLI

La principale differenza teorica è che C # è progettato con loop mentre F # è progettato sui principi del Lambda calcolo. Per un ottimo libro sui principi del Lambda Calculus, vedere questo libro gratuito: Struttura e interpretazione dei programmi per computer, di Abelson, Sussman e Sussman .

Per un ottimo articolo introduttivo sulle chiamate di coda in F #, vedere questo articolo: Introduzione dettagliata alle chiamate di coda in F # . Infine, ecco un articolo che copre la differenza tra ricorsione non di coda e ricorsione di chiamata di coda (in F #): Ricorsione di coda vs. ricorsione non di coda in F diesis .


8

Recentemente mi è stato detto che il compilatore C # per 64 bit ottimizza la ricorsione in coda.

C # implementa anche questo. Il motivo per cui non viene sempre applicato è che le regole utilizzate per applicare la ricorsione della coda sono molto rigide.


8
Il jitter x64 fa questo, ma il compilatore C # no
Mark Sowul,

grazie per l'informazione. Questo è bianco diverso da quello che pensavo in precedenza.
Alexandre Brisebois,

3
Giusto per chiarire questi due commenti, C # non emette mai il codice operativo CIL 'tail' e credo che questo sia ancora vero nel 2017. Tuttavia, per tutte le lingue, tale codice operativo è sempre di avviso solo nel senso che i rispettivi nervosismo (x86, x64 ) lo ignorerà silenziosamente se varie condizioni non sono soddisfatte (beh, nessun errore tranne il possibile overflow dello stack ). Questo spiega perché sei costretto a seguire "tail" con "ret" - è per questo caso. Nel frattempo, i nervosisti sono anche liberi di applicare l'ottimizzazione quando non c'è il prefisso "tail" nel CIL, sempre come ritenuto appropriato e indipendentemente dal linguaggio .NET.
Glenn Slayden

3

È possibile utilizzare la tecnica del trampolino per le funzioni ricorsive della coda in C # (o Java). Tuttavia, la soluzione migliore (se ti interessa solo l'utilizzo dello stack) è usare questo piccolo metodo di supporto per racchiudere parti della stessa funzione ricorsiva e renderla iterativa mantenendo la funzione leggibile.


I trampolini sono invasivi (sono una modifica globale alla convenzione di chiamata), ~ 10 volte più lenti della corretta eliminazione delle chiamate di coda e offuscano tutte le informazioni sulla traccia dello stack rendendo molto più difficile il debug e il codice del profilo
JD

1

Come altre risposte menzionate, CLR supporta l'ottimizzazione delle chiamate di coda e sembra che storicamente fosse in progressivo miglioramento. Ma supportarlo in C # ha un Proposalproblema aperto nel repository git per la progettazione del linguaggio di programmazione C # Support tail recursion # 2544 .

Puoi trovare alcuni dettagli e informazioni utili lì. Ad esempio @jaykrell menzionato

Fammi dare quello che so.

A volte tailcall è una performance vincente. Può risparmiare CPU. jmp è più economico di call / ret Può salvare lo stack. Toccando meno pila si migliora la località.

A volte tailcall è una perdita di prestazioni, stack win. Il CLR ha un meccanismo complesso in cui passare più parametri al chiamato rispetto a quelli ricevuti dal chiamante. Intendo specificamente più spazio nello stack per i parametri. Questo è lento. Ma conserva lo stack. Lo farà solo con la coda. prefisso.

Se i parametri del chiamante sono più grandi dello stack dei parametri del chiamato, di solito si tratta di una trasformazione win-win piuttosto semplice. Potrebbero esserci fattori come il cambiamento della posizione del parametro da gestito a intero / float e la generazione di StackMap precisi e simili.

Ora, c'è un altro angolo, quello degli algoritmi che richiedono l'eliminazione della tailcall, allo scopo di essere in grado di elaborare dati arbitrariamente grandi con stack fisso / piccolo. Non si tratta di prestazioni, ma di capacità di correre.

Consentitemi anche di menzionare (come informazioni aggiuntive), quando generiamo un lambda compilato utilizzando classi di espressioni nello System.Linq.Expressionsspazio dei nomi, c'è un argomento chiamato 'tailCall' che, come spiegato nel suo commento, è

Un valore bool che indica se l'ottimizzazione della chiamata di coda verrà applicata durante la compilazione dell'espressione creata.

Non l'ho ancora provato e non sono sicuro di come possa aiutare in relazione alla tua domanda, ma probabilmente qualcuno può provarlo e potrebbe essere utile in alcuni scenari:


var myFuncExpression = System.Linq.Expressions.Expression.Lambda<Func<  >>(body:  , tailCall: true, parameters:  );

var myFunc =  myFuncExpression.Compile();
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.