2015-02-21 14 views
8

Zabrakło mi w dziwny spadku wydajności że mam sprowadzić do tego kodu:Wydajność kara kiedy Generic.List <T> .Add jest ostatnia wypowiedź w funkcji i tailcall optymalizacja jest

[<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 

Różnica między add i add1 jest dodatkowym () na końcu.

Kiedy budować go jako kompilacji x64 wydania użyciem F # .NET 3.1 na 4.5.1 uzyskać ten wynik:

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

Rejestracja rodzaju List<T>.Add jest T -> unit Spodziewam się, że add i add1 powinien zachowywać się identycznie .

Stosując ILdasm Zostało stwierdzone, że add sporządza się (w tym tylko odpowiedniej części)

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) 

podczas add1 do

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) 

to znaczy bez "połączenia ogona". Kiedy więc wyłączam optymalizację połączeń końcowych, zarówno add, jak i add1 działają z tą samą prędkością.

Dlaczego instrukcja tail. powoduje, że wywołanie funkcji jest znacznie wolniejsze? Czy jest to błąd lub funkcja?


EDYCJA: To jest oryginalny kod tutaj zauważyłem to zachowanie. Kiedy wartość true na końcu jest opuszczona, wykazuje taki sam spadek wydajności jak powyższy kod.

let makeAtom (ctx: CleanCifContext) (element: CleanCifAtomSiteElement) = 
    let residue = getResidue ctx element 

    let position = 
    Vector3(float32 (element.PositionX.ValueOrFail()), float32 (element.PositionY.ValueOrFail()), float32 (element.PositionZ.ValueOrFail())) 
    let atom = 
    CifAtom(id = ctx.Atoms.Count, element = element.ElementSymbol.ValueOrFail(), 
      residue = residue, serialNumber = element.Id.ValueOrFail(), 
      name = element.Name.ValueOrFail(), authName = element.AuthName.Value(), altLoc = element.AltLoc.Value(), 
      occupancy = float32 (element.Occupancy.ValueOrFail()), tempFactor = float32 (element.TempFactor.ValueOrFail())) 

    ctx.Atoms.Add atom 
    ctx.Positions.Add position 
    true 
+0

Interesujące, występuje tylko w .NET 4+, a rozbieżność jest znacznie mniejsza na x86 lub podczas korzystania z innych typów danych w "liście". –

+0

@DaxFohl Tak, zauważyłem, że jest również niższy na x86.Ale potrzebuję, aby mój kod był 64-bitowy, dlatego uwzględniłem te dane. – Dave

+0

Czy uruchomiłeś go na RyuJIT lub na zwykłym starym JIT? Artykuł, który łączysz, wydaje się być powiązany ze starym. –

Odpowiedz

2

myślę, że zorientowali się, gdzie jest problem i dlaczego to jest moje niezrozumienie problemu raczej niż błąd w F # kompilatora lub .NET.

Kod

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

oznacza w przybliżeniu "połączenie List<T>.Add z położenia tailcall od wartości Vector3(j, j, j)" podczas

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

oznacza "połączenie List<T>.Add od wartości Vector3(j, j, j) a następnie powrócić unit".

Type-mądry, nie ma różnicy jak List<T>.Add zwrotów unit więc nieprawidłowo założonych positions.Add dostanie nazywa a następnie add zwróci wartość unit która jest wartość zwracana List<T>.Add. Jednak, jak stwierdzono w http://blogs.msdn.com/b/clrcodegeneration/archive/2009/05/11/tail-call-improvements-in-net-framework-4.aspx, JIT musi wykonać pewną "magię stosu", gdy argumenty funkcji zwanej ogonem nie są trywialne. I właśnie z tego wynika luka wydajności. Różnica jest bardzo subtelna, ale ona istnieje.