2015-05-29 22 views
12

Die folgende C# Funktion:Was ist der Zweck der zusätzlichen LDNULL und Tail. in F # Implementierung vs C#?

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

kompiliert unsurprisingly dazu:

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

Aber die äquivalente # Funktion F:

let resultOfFunc func = func() 

dazu kompiliert:

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

(Beide sind im Freigabemodus). Es gibt ein extra Nop am Anfang, auf das ich nicht sonderlich neugierig bin, aber das Interessante ist die extra Anleitung ldnull und tail..

Meine Vermutung (wahrscheinlich falsch) ist, dass ldnull, falls notwendig, ist die Funktion void ist so still es gibt etwas (unit), aber das erklärt nicht, was der Zweck der tail. Anweisung ist. Und was passiert, wenn die Funktion etwas auf den Stapel drückt, ist es nicht mit einer zusätzlichen Null verbunden, die nicht geknallt wird?

+3

Ich vermute, dass in diesem Fall die Funktion zu einem Tail-Aufruf konvertiert wurde - die neue Funktion verwendet den Stack-Bereich des alten –

+1

Beachten Sie, dass dies wahrscheinlich eine Performance-Pessimierung verursacht, siehe diese Frage: [Leistung Penalty, wenn Generic.List .Add ist die letzte Anweisung in einer Funktion und die Optimierung der Endlosschleife ist aktiviert] (http://stackoverflow.com/q/28649422/636019) – ildjarn

Antwort

17

Die C# - und F # -Versionen haben eine wichtige Unterscheidung: Die C# -Funktion hat keine Parameter, aber die F # -Version hat einen Parameter vom Typ unit. Dieser unit Wert ist, was als ldnull angezeigt wird (weil null wird als Darstellung der nur unit Wert verwendet, ()).

Wenn Sie die zweite Funktion in C# übersetzen, würde es so aussehen:

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

Was die .tail Anweisung - das ist so genannte „Tail Call-Optimierung“.
Während eines normalen Funktionsaufrufs wird eine Rücksprungadresse auf den Stapel (den CPU-Stapel) geschoben und dann wird die Funktion aufgerufen. Wenn die Funktion fertig ist, führt sie den "Return" -Befehl aus, der die Rücksprungadresse vom Stapel löscht und die Steuerung dorthin überträgt.
Wenn jedoch Funktion A Anrufe B funktionieren, und dann sofort Funktion B ‚s Rückgabewert zurückgibt, ohne etwas anderes zu tun, kann die CPU überspringen auf dem Stapel die zusätzliche Rückkehradresse drücken, und führen Sie einen‚Sprung‘zu B anstelle von "Anruf". Auf diese Weise, wenn B die Anweisung "return" ausführt, wird die CPU die Rücksprungadresse vom Stapel löschen, und diese Adresse wird nicht auf A verweisen, sondern auf diejenige, die zuerst A aufgerufen hat.
Eine weitere Möglichkeit, darüber nachzudenken ist: function A Anrufe funktionieren B nicht vor Rückkehr, aber statt Rückkehr und damit die Delegierten die Ehre zu B zurück.

Also diese magische Technik ermöglicht es uns, einen Anruf ohne einen Platz auf dem Stapel zu konsumieren, was bedeutet, dass Sie beliebig viele solcher Aufrufe durchführen können, ohne Stapelüberlauf zu riskieren. Dies ist sehr wichtig in der funktionalen Programmierung, da es rekursive Algorithmen effizient implementieren kann.

Es heißt „Endrekursion“, weil Aufruf B geschieht, so zu sagen, am Heck von A.