2015-02-21 2 views
8

Ich habe in eine seltsame Leistungseinbuße führen, das ich gekocht habe auf diesen Code unten:Leistungseinbuße, wenn Generic.List <T> .Add die die letzte Anweisung in einer Funktion und tailcall Optimierung ist auf

[<Struct>] 
type Vector3(x: float32, y: float32, z: float32) = 
    member this.X = x 
    member this.Y = y 
    member this.Z = z 

type Data(n: int) = 
    let positions = System.Collections.Generic.List<Vector3>() 
    let add j = positions.Add (Vector3(j, j, j)) 
    let add1 j = positions.Add (Vector3(j, j, j));() 
    member this.UseAdd() = for i = 1 to n do add (float32 i) 
    member this.UseAdd1() = for i = 1 to n do add1 (float32 i) 

let timeIt name (f: unit -> unit) = 
    let timer = System.Diagnostics.Stopwatch.StartNew() 
    f() 
    printfn "%s: %ims" name (int timer.ElapsedMilliseconds) 

let test() = 
    for i = 1 to 3 do timeIt "ADD" (fun() -> Data(1000000).UseAdd()) 
    for i = 1 to 3 do timeIt "ADD1" (fun() -> Data(1000000).UseAdd1()) 

[<EntryPoint>] 
let main argv = 
    test() 
    0 

Der Unterschied zwischen add und add1 ist das extra () am Ende.

Wenn ich es als x64 Releasebuild bauen mit F # 3.1 auf .NET 4.5.1 bekomme ich diese Ausgabe:

ADD: 461ms 
ADD: 457ms 
ADD: 450ms 
ADD1: 25ms 
ADD1: 26ms 
ADD1: 16ms 

Da die Art der List<T>.Add ist T -> unit Ich würde erwarten, dass add und add1 sollten identisch verhalten .

Mit ildasm Ich habe festgestellt, dass add kompiliert (einschließlich nur der relevante Teil)

IL_000a: newobj  instance void Program/Vector3::.ctor(float32, 
                  float32, 
                  float32) 
IL_000f: tail. 
IL_0011: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<valuetype Program/Vector3>::Add(!0) 

während add1 in

IL_000a: newobj  instance void Program/Vector3::.ctor(float32, 
                  float32, 
                  float32) 
IL_000f: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<valuetype Program/Vector3>::Add(!0) 

das heißt ohne "Endaufruf". Wenn ich also die Tail Call Optimierung abstelle, laufen sowohl add als auch add1 mit der gleichen Geschwindigkeit.

Warum bewirkt der Befehl tail., dass der Funktionsaufruf so viel langsamer ist? Ist das ein Fehler oder eine Funktion?


EDIT: Dies ist der ursprüngliche Code hier bemerkte ich dieses Verhalten. Wenn der true Wert am Ende gelöscht wird, zeigt es den gleichen Leistungsabfall wie der obige Code.

+0

Interessant, es tritt nur in .NET 4+, und die Diskrepanz ist auf x86 oder bei Verwendung anderer Datentypen in der 'List' viel kleiner. –

+0

@DaxFohl Ja, ich habe festgestellt, dass es auch auf x86 niedriger ist.Aber ich brauche meinen Code 64-Bit, deshalb habe ich diese Daten enthalten. – Dave

+0

Hast du es auf RyuJIT oder auf normalem JIT ausgeführt? Der Artikel, den Sie verlinkt haben, scheint mit dem alten verwandt zu sein. –

Antwort

2

Ich denke, ich habe herausgefunden, wo das Problem liegt und warum es ist mein Missverständnis des Problems eher als Fehler in der F # Compiler oder .NET.

Der Code

let add j = positions.Add (Vector3(j, j, j)) 

bedeutet "nennen List<T>.Add aus der tailcall Position auf dem Wert Vector3(j, j, j)" grob während

let add1 j = positions.Add (Vector3(j, j, j));() 

bedeutet "Vector3(j, j, j)List<T>.Add auf den Wert rufen und dann unit zurückkehren".

Typ-weise, gibt es keinen Unterschied, wie List<T>.Add kehrt unit so falsch angenommen, ich positions.Add und add zurückkehren würde den Wert unit das ist der Rückgabewert von List<T>.Add dann aufgerufen würde. Wie jedoch bei http://blogs.msdn.com/b/clrcodegeneration/archive/2009/05/11/tail-call-improvements-in-net-framework-4.aspx angegeben, muss das JIT einige "Stapelmagie" ausführen, wenn die Argumente der Schwanz-aufgerufenen Funktion nicht-trivial sind. Und hier kommt die Leistungslücke. Der Unterschied ist sehr subtil, aber es ist da.

Verwandte Themen