2015-05-29 38 views
12

Poniższy C# funkcja:Jaki jest cel dodatkowego ldnull i ogona. w F # realizacji vs C#?

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

kompiluje dziwi to:

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

ale równoważnej F # Funkcja:

let resultOfFunc func = func() 

gromadzi do tego:

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

(Obie są w trybie zwolnienia). Na początku jest dodatkowy nop, którego nie jestem zbyt ciekawy, ale interesujące są dodatkowe instrukcje: ldnull i tail..

Domyślam (prawdopodobnie źle) jest to, że ldnull jest konieczne w przypadku, gdy funkcja jest void więc nadal zwraca coś (unit), ale to nie wyjaśnia, co jest celem instrukcji tail.. A co się dzieje, gdy funkcja przesuwa coś na stos, czy nie utknęła z dodatkowym pustym miejscem, które nie zostanie zerwane?

+3

Podejrzewam, że w tym przypadku funkcja została przekształcona w wywołanie ogona - nowa funkcja uzyskuje dostęp do przestrzeni stosu starej. –

+1

Należy pamiętać, że może to spowodować pesymalizację wydajności, zobacz to pytanie: [Wydajność kara, gdy Generic.List .Add to ostatnia instrukcja funkcji i optymalizacja tailcall jest włączona] (http://stackoverflow.com/q/28649422/636019) – ildjarn

Odpowiedz

17

Wersje C# i F # mają ważne rozróżnienie: funkcja C# nie ma żadnych parametrów, ale wersja F # ma jeden parametr typu unit. Ta wartość to unit, która pojawia się jako ldnull (ponieważ null jest używana jako reprezentacja jedynej wartości unit, ()).

Jeśli było tłumaczyć drugą funkcję C#, to będzie wyglądać następująco:

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

jak dla instrukcji .tail - czyli tak zwany "tail optymalizacji Call".
Podczas zwykłego wywołania funkcji, adres zwrotny zostaje przesunięty na stos (stos CPU), a następnie wywoływana jest funkcja. Gdy funkcja jest zakończona, wykonuje instrukcję "return", która wywołuje adres zwrotny ze stosu i przekazuje tam kontrolę.
Jednak gdy funkcja A wywołuje funkcję B, a następnie natychmiast funkcja zwraca B „s wartości zwracanej, nie robiąc nic innego, CPU może pominąć wciskając dodatkowy adres powrotu na stosie, i wykonać«skok»do B zamiast "połączenie". W ten sposób, gdy B wykona polecenie "return", CPU wyśle ​​adres zwrotny ze stosu, a ten adres wskaże nie A, ale na to, kto pierwszy nazwał A.
Inny sposób myślenia o tym to: funkcja A wywołuje funkcję B nie przed powrocie, ale zamiast powrocie, a zatem deleguje cześć powrotu do B.

Tak więc ta magiczna technika pozwala nam wykonywać połączenia bez zużywania miejsca na stosie, co oznacza, że ​​można wykonywać dowolnie wiele takich połączeń bez ryzyka przepełnienia stosu. Jest to bardzo ważne w programowaniu funkcjonalnym, ponieważ pozwala efektywnie implementować algorytmy rekurencyjne.

Nazywa się "ogon wywołanie", ponieważ wywołanie B dzieje się, aby tak powiedzieć, na ogonie z A.