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
Nicht sicher, warum, aber es läuft 0,3s wenn kompiliert x86 und 1,4s wenn kompiliert x64. –
Laufzeitaufwand? – Aybe
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. –