2017-09-21 1 views
18

Die folgende F # Programm definiert eine Funktion, die die kleinere der zwei Paare von ints als struct Tupeln dargestellt gibt und es nimmt 1.4S auszuführen:Leistung von struct Tupeln

let [<EntryPoint>] main _ = 
    let min a b : int = if a < b then a else b 
    let min (struct(a1, b1)) (struct(a2, b2)) = struct(min a1 a2, min b1 b2) 
    let mutable x = struct(0, 0) 
    for i in 1..100000000 do 
    x <- min x (struct(i, i)) 
    0 

Wenn ich die CIL bis C# I decompile erhalten diesen Code:

public static int MinInt(int a, int b) 
    { 
     if (a < b) 
     { 
      return a; 
     } 
     return b; 
    } 

    public static System.ValueTuple<int, int> MinPair(System.ValueTuple<int, int> _arg2, System.ValueTuple<int, int> _arg1) 
    { 
     int b = _arg2.Item2; 
     int a = _arg2.Item1; 
     int b2 = _arg1.Item2; 
     int a2 = _arg1.Item1; 
     return new System.ValueTuple<int, int>(MinInt(a, a2), MinInt(b, b2)); 
    } 

    public static void Main(string[] args) 
    { 
     System.ValueTuple<int, int> x = new System.ValueTuple<int, int>(0, 0); 
     for (int i = 1; i <= 100000000; i++) 
     { 
      x = MinPair(x, new System.ValueTuple<int, int>(i, i)); 
     } 
    } 

neu zu kompilieren, dass es dauert nur 0,3 Sekunden mit dem C# -Compiler, die älter als 4x schneller als das Original F # ist.

Ich kann nicht sehen, warum ein Programm viel schneller als das andere ist. Ich habe sogar beide Versionen zu CIL dekompiliert und kann keinen offensichtlichen Grund sehen. Der Aufruf der C# Min-Funktion von F # führt zu derselben (schlechten) Leistung. Die CIL der inneren Schleife des Aufrufers sind buchstäblich identisch.

Kann jemand diesen wesentlichen Leistungsunterschied erklären?

+4

Nicht sicher, warum, aber es läuft 0,3s wenn kompiliert x86 und 1,4s wenn kompiliert x64. –

+0

Laufzeitaufwand? – Aybe

+0

Ein einzelner Lauf ist nicht genug, um irgendwelche Schlussfolgerungen zu erzielen. Verwenden Sie BenchmarkDotNet, um genügend aussagekräftige Daten zu sammeln, damit Sie einen Vergleich durchführen können. Posten * diese * Statistiken. –

Antwort

7

Sie führen beide Beispiele in derselben Architektur aus. Ich bekomme ~ 1.4sec auf x64 für beide F # und C# -Code und ~ 0.6sec auf x86 für F # und ~ 0.3sec auf x86 für C#.

Wie Sie sagen, wenn decompiling die Baugruppen der Code awefully ähnlich sieht, aber einige dissimilarties erscheinen, wenn die IL-Code Prüfung:

F # - let min (struct(a1, b1)) (struct(a2, b2)) ...

.maxstack 5 
.locals init (
    [0] int32 b1, 
    [1] int32 a1, 
    [2] int32 b2, 
    [3] int32 a2 
) 

IL_0000: ldarga.s _arg2 
IL_0002: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2 
IL_0007: stloc.0 
IL_0008: ldarga.s _arg2 
IL_000a: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1 
IL_000f: stloc.1 
IL_0010: ldarga.s _arg1 
IL_0012: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2 
IL_0017: stloc.2 
IL_0018: ldarga.s _arg1 
IL_001a: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1 
IL_001f: stloc.3 
IL_0020: nop 
IL_0021: ldloc.1 
IL_0022: ldloc.3 
IL_0023: call int32 Program::[email protected](int32, int32) 
IL_0028: ldloc.0 
IL_0029: ldloc.2 
IL_002a: call int32 Program::[email protected](int32, int32) 
IL_002f: newobj instance void valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::.ctor(!0, !1) 
IL_0034: ret 

C# - MinPair

.maxstack 3 
.locals init (
    [0] int32 b, 
    [1] int32 b2, 
    [2] int32 a2 
) 

IL_0000: ldarg.0 
IL_0001: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2 
IL_0006: stloc.0 
IL_0007: ldarg.0 
IL_0008: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1 
IL_000d: ldarg.1 
IL_000e: ldfld !1 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item2 
IL_0013: stloc.1 
IL_0014: ldarg.1 
IL_0015: ldfld !0 valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::Item1 
IL_001a: stloc.2 
IL_001b: ldloc.2 
IL_001c: call int32 PerfItCs.Program::MinInt(int32, int32) 
IL_0021: ldloc.0 
IL_0022: ldloc.1 
IL_0023: call int32 PerfItCs.Program::MinInt(int32, int32) 
IL_0028: newobj instance void valuetype [System.ValueTuple]System.ValueTuple`2<int32, int32>::.ctor(!0, !1) 
IL_002d: ret 

Der Unterschied hier ist, dass der C# -Compiler vermeidet, einige lokale Variablen einzuführen, indem er die Zwischenergebnisse auf die s drückt Heftzwecke. Da lokale Variablen auf dem Stack sowieso zugewiesen sind, ist es schwer zu verstehen, warum dies zu einem effizienteren Code führen sollte.

Die anderen Funktionen sind sehr ähnlich.

Auseinanderbauen der x86 ergibt dies:

F # - die Schleife

; F# 
; struct (i, i) 
01690a7e 8bce   mov  ecx,esi 
01690a80 8bd6   mov  edx,esi 
; Loads x (pair) onto stack 
01690a82 8d45f0   lea  eax,[ebp-10h] 
01690a85 83ec08   sub  esp,8 
01690a88 f30f7e00  movq xmm0,mmword ptr [eax] 
01690a8c 660fd60424  movq mmword ptr [esp],xmm0 
; Push new tuple on stack 
01690a91 52    push edx 
01690a92 51    push ecx 
; Loads pointer to x into ecx (result will be written here) 
01690a93 8d4df0   lea  ecx,[ebp-10h] 
; Call min 
01690a96 ff15744dfe00 call dword ptr ds:[0FE4D74h] 
; Increase i 
01690a9c 46    inc  esi 
01690a9d 81fe01e1f505 cmp  esi,offset FSharp_Core_ni+0x6be101 (05f5e101) 
; Reached the end? 
01690aa3 7cd9   jl  01690a7e 

C# - die Schleife

; C# 
; Loads x (pair) into ecx, eax 
02c2057b 8d55ec   lea  edx,[ebp-14h] 
02c2057e 8b0a   mov  ecx,dword ptr [edx] 
02c20580 8b4204   mov  eax,dword ptr [edx+4] 
; new System.ValueTuple<int, int>(i, i) 
02c20583 8bfe   mov  edi,esi 
02c20585 8bd6   mov  edx,esi 
; Push x on stack 
02c20587 50    push eax 
02c20588 51    push ecx 
; Push new tuple on stack 
02c20589 52    push edx 
02c2058a 57    push edi 
; Loads pointer to x into ecx (result will be written here) 
02c2058b 8d4dec   lea  ecx,[ebp-14h] 
; Call MinPair 
02c2058e ff15104d2401 call dword ptr ds:[1244D10h] 
; Increase i 
02c20594 46    inc  esi 
; Reached the end? 
02c20595 81fe00e1f505 cmp  esi,5F5E100h 
02c2059b 7ede   jle  02c2057b 

Es ist schwer zu verstehen, warum F # -Code hier deutlich schlechter durchführen soll. Der Code sieht ungefähr so ​​aus wie die Ausnahme, wie x auf den Stack geladen wird. Bis jemand eine gute Erklärung dazu vorlegt, warum ich spekulieren werde, dass sein movq eine schlechtere Latenz als push hat und da alle Befehle den Stack manipulieren, kann die CPU die Anweisungen zur Verringerung der Latenz von movq nicht neu ordnen.

Warum wählte der Jitter movq für den F # -Code und nicht für den C# -Code, den ich derzeit nicht kenne.

Für x64 scheint die Leistung aufgrund von mehr Overhead in den Methoden Preludes und mehr Abwürgen wegen Aliasing zu verschlechtern. Dies ist hauptsächlich Spekulation von meiner Seite, aber es ist schwer aus dem Assembly-Code zu sehen, außer das Abwürgen könnte die Leistung von x64 um den Faktor 4x verringern.

Durch Markierung min als Inline sowohl x64 und x86 läuft in ~ 0,15 s. Nicht überraschend, da dies den Overhead von Methodenvorschlägen und viel Lesen und Schreiben auf dem Stapel eliminiert.

Markierung F # -Methoden für aggressive Inlining (mit [MethodImpl (MethodImplOptions.AggressiveInlining)]) funktioniert nicht, da der F # -Compiler alle solche Attribute entfernt, dh der Jitter sieht ihn nie, aber die C# -Methoden für aggressive Inlining macht den C# -Code in ~ 0,15 Sek. Ausgeführt.

Also entschied sich der x86-Jitter aus irgendeinem Grund dafür, den Code anders zu machen, obwohl der IL-Code sehr ähnlich aussieht. Möglicherweise beeinflussen die Attribute auf den Methoden den Jitter, da sie ein bisschen anders sind.

Der x64-Jitter könnte wahrscheinlich einen besseren Job machen, wenn er die Parameter auf dem Stack effizienter anpatscht. Ich denke, mit push als x86 Jitter ist vorzuziehen über mov als die Semantik von push ist mehr eingeschränkt, aber das ist nur Spekulation meinerseits.

In solchen Fällen, wenn die Methoden billig sind, sie als Inline zu markieren, kann gut sein.

Um ehrlich zu sein, ich bin mir nicht sicher, ob dies OP hilft, aber hoffentlich war es etwas interessant.

PS. Ich führe den Code auf .NET 4.6.2 auf einem i5 3570K

+0

"Führen Sie beide Beispiele in derselben Architektur aus". Ich spiele jetzt nur mit Architekturen. Wenn ich beide in x64 laufe, sehe ich immer noch C# schneller, aber nur 1,3 Sekunden gegen 1,9 Sekunden. In x86 laufe ich 0,3s vs 1,2s wie vorher. Interessanterweise macht das Verschieben des Codes einen großen Unterschied: Ich kann ihn durch Refactoring (!) Buchstäblich 10x langsamer machen. –

+1

Der Leistungsschub ist wahrscheinlich, wenn der Jitter entscheidet, die Funktion zu inline oder nicht. 'min' ist billig im Vergleich zur Einrichtung des Anrufs, Anrufs, des Prologs, des Epilogs und der Rückkehr. – FuleSnabel

+0

Wie hast du die Asm bekommen, BTW? –