2015-05-29 10 views
12

La seguente C# funzione:Qual è lo scopo di extra ldnull e tail. in F # implementazione vs C#?

T ResultOfFunc<T>(Func<T> f) 
{ 
    return f(); 
} 

compila sorprende a questo:

IL_0000: ldarg.1  
IL_0001: callvirt 05 00 00 0A 
IL_0006: ret 

Ma l'equivalente F # funzione:

let resultOfFunc func = func() 

compila a questo:

IL_0000: nop   
IL_0001: ldarg.0  
IL_0002: ldnull  
IL_0003: tail.  
IL_0005: callvirt 04 00 00 0A 
IL_000A: ret 

(Entrambi sono in modalità di rilascio). C'è un extra nop all'inizio che non sono troppo curioso, ma la cosa interessante sono le istruzioni aggiuntive ldnull e tail..

mia ipotesi (probabilmente sbagliato) è che ldnull è necessaria nel caso in cui la funzione è void quindi ritorna ancora qualcosa (unit), ma questo non spiega qual è lo scopo dell'istruzione tail.. E cosa succede se la funzione spinge qualcosa in pila, non è bloccato con un valore null extra che non viene visualizzato?

+3

Sospetto che in questo caso la funzione sia stata convertita in una chiamata di coda: la nuova funzione ottiene lo spazio di stack del precedente –

+1

Nota che questo potrebbe causare una pessimizzazione delle prestazioni, vedere questa domanda: [Prestazioni penalità quando Generic.List .Add è l'ultima istruzione in una funzione e l'ottimizzazione di tailcall è attiva] (http://stackoverflow.com/q/28649422/636019) – ildjarn

risposta

17

Le versioni C# e F # hanno un'importante distinzione: la funzione C# non ha parametri, ma la versione F # ha un parametro di tipo unit. Il valore unit è quello che viene visualizzato come ldnull (poiché null viene utilizzato come rappresentazione del solo valore unit, ()).

Se si dovesse tradurre la seconda funzione di C#, sarebbe simile a questa:

T ResultOfFunc<T>(Func<Unit, T> f) { 
    return f(null); 
} 

Per quanto riguarda l'istruzione .tail - che è la cosiddetta "ottimizzazione chiamata coda".
Durante una normale chiamata di funzione, un indirizzo di ritorno viene inserito nello stack (lo stack della CPU) e quindi viene richiamata la funzione. Quando la funzione è completata, esegue l'istruzione "return", che apre l'indirizzo di ritorno dallo stack e trasferisce il controllo lì.
Tuttavia, quando la funzione A chiamate di funzione B, e poi restituisce immediatamente il valore di ritorno la funzione s' B, senza fare niente altro, la CPU può saltare spingendo l'indirizzo di ritorno in più sullo stack, e di eseguire una 'salto' per B invece di una "chiamata". In questo modo, quando B esegue l'istruzione "return", la CPU visualizzerà l'indirizzo di ritorno dallo stack e quell'indirizzo punterà non su A, ma a chiunque abbia chiamato A in primo luogo.
Un altro modo di pensare a questo proposito è la seguente: la funzione A chiamate di funzione B non prima di tornare, ma invece di ritorno, e quindi i delegati l'onore di tornare a B.

Quindi, in effetti, questa tecnica magia ci permette di fare una chiamata senza consumare un punto sulla pila, il che significa che è possibile eseguire un numero arbitrario di tali chiamate senza rischiare overflow dello stack. Questo è molto importante nella programmazione funzionale, perché consente di implementare in modo efficiente algoritmi ricorsivi.

Si chiama "chiamata coda", perché chiamata a B accade, per così dire, alla coda di A.