Intro: Ich schreibe Hochleistungscode in C #. Ja, ich weiß, dass C ++ mir eine bessere Optimierung bieten würde, aber ich entscheide mich trotzdem für C #. Ich möchte diese Wahl nicht diskutieren. Ich würde eher von denen hören, die wie ich versuchen, Hochleistungscode in .NET Framework zu schreiben.
Fragen:
- Warum ist der Operator im folgenden Code langsamer als der entsprechende Methodenaufruf?
- Warum übergibt die Methode, die im folgenden Code zwei Doubles übergibt, schneller als die entsprechende Methode eine Struktur, die zwei Doubles enthält? (A: Ältere JITs optimieren Strukturen schlecht)
- Gibt es eine Möglichkeit, den .NET JIT-Compiler dazu zu bringen, einfache Strukturen genauso effizient zu behandeln wie die Mitglieder der Struktur? (A: neuere JIT bekommen)
Was ich zu wissen glaube: Der ursprüngliche .NET JIT-Compiler würde nichts einbinden, was eine Struktur betrifft. Bizarr gegebene Strukturen sollten nur verwendet werden, wenn Sie kleine Werttypen benötigen, die wie integrierte Funktionen optimiert werden sollten, aber wahr sind. Glücklicherweise haben sie in .NET 3.5SP1 und .NET 2.0SP2 einige Verbesserungen am JIT-Optimierer vorgenommen, einschließlich Verbesserungen am Inlining, insbesondere für Strukturen. (Ich vermute, sie haben das getan, weil sonst die neue Complex-Struktur, die sie eingeführt haben, eine schreckliche Leistung erbracht hätte ... also hat das Complex-Team wahrscheinlich auf das JIT Optimizer-Team geschlagen.) Daher ist wahrscheinlich jede Dokumentation vor .NET 3.5 SP1 nicht zu relevant für dieses Problem.
Was meine Tests zeigen: Ich habe überprüft, ob ich das neuere JIT-Optimierungsprogramm habe, indem ich überprüft habe, ob die Datei C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll die Version> = 3053 hat und daher diese Verbesserungen aufweisen sollte zum JIT-Optimierer. Trotzdem zeigen meine Timings und Blicke auf die Demontage:
Der von JIT erzeugte Code zum Übergeben einer Struktur mit zwei Doubles ist weitaus weniger effizient als Code, der die beiden Doubles direkt übergibt.
Der von JIT erzeugte Code für eine struct-Methode übergibt 'this' weitaus effizienter, als wenn Sie eine struct als Argument übergeben hätten.
Die JIT wird immer noch besser inline, wenn Sie zwei Doubles übergeben, anstatt eine Struktur mit zwei Doubles zu übergeben, selbst wenn sich der Multiplikator eindeutig in einer Schleife befindet.
Die Timings: Wenn ich mir die Demontage anschaue, stelle ich fest, dass die meiste Zeit in den Schleifen nur auf die Testdaten aus der Liste zugegriffen wird. Der Unterschied zwischen den vier Arten, dieselben Anrufe zu tätigen, ist dramatisch unterschiedlich, wenn Sie den Overhead-Code der Schleife und den Zugriff auf die Daten herausrechnen. Ich bekomme 5x bis 20x Beschleunigung für PlusEqual (doppelt, doppelt) anstelle von PlusEqual (Element). Und 10x bis 40x für PlusEqual (double, double) anstelle von Operator + =. Beeindruckend. Traurig.
Hier ist ein Satz von Timings:
Populating List<Element> took 320ms.
The PlusEqual() method took 105ms.
The 'same' += operator took 131ms.
The 'same' -= operator took 139ms.
The PlusEqual(double, double) method took 68ms.
The do nothing loop took 66ms.
The ratio of operator with constructor to method is 124%.
The ratio of operator without constructor to method is 132%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 64%.
If we remove the overhead time for the loop accessing the elements from the List...
The ratio of operator with constructor to method is 166%.
The ratio of operator without constructor to method is 187%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 5%.
Der Code:
namespace OperatorVsMethod
{
public struct Element
{
public double Left;
public double Right;
public Element(double left, double right)
{
this.Left = left;
this.Right = right;
}
public static Element operator +(Element x, Element y)
{
return new Element(x.Left + y.Left, x.Right + y.Right);
}
public static Element operator -(Element x, Element y)
{
x.Left += y.Left;
x.Right += y.Right;
return x;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(Element that)
{
this.Left += that.Left;
this.Right += that.Right;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(double thatLeft, double thatRight)
{
this.Left += thatLeft;
this.Right += thatRight;
}
}
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Stopwatch stopwatch = new Stopwatch();
// Populate a List of Elements to multiply together
int seedSize = 4;
List<double> doubles = new List<double>(seedSize);
doubles.Add(2.5d);
doubles.Add(100000d);
doubles.Add(-0.5d);
doubles.Add(-100002d);
int size = 2500000 * seedSize;
List<Element> elts = new List<Element>(size);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
int di = ii % seedSize;
double d = doubles[di];
elts.Add(new Element(d, d));
}
stopwatch.Stop();
long populateMS = stopwatch.ElapsedMilliseconds;
// Measure speed of += operator (calls ctor)
Element operatorCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorCtorResult += elts[ii];
}
stopwatch.Stop();
long operatorCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of -= operator (+= without ctor)
Element operatorNoCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorNoCtorResult -= elts[ii];
}
stopwatch.Stop();
long operatorNoCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(Element) method
Element plusEqualResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
plusEqualResult.PlusEqual(elts[ii]);
}
stopwatch.Stop();
long plusEqualMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(double, double) method
Element plusEqualDDResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
plusEqualDDResult.PlusEqual(elt.Left, elt.Right);
}
stopwatch.Stop();
long plusEqualDDMS = stopwatch.ElapsedMilliseconds;
// Measure speed of doing nothing but accessing the Element
Element doNothingResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
double left = elt.Left;
double right = elt.Right;
}
stopwatch.Stop();
long doNothingMS = stopwatch.ElapsedMilliseconds;
// Report results
Assert.AreEqual(1d, operatorCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, operatorNoCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualDDResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, doNothingResult.Left, "The operator += did not compute the right result!");
// Report speeds
Console.WriteLine("Populating List<Element> took {0}ms.", populateMS);
Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS);
Console.WriteLine("The 'same' += operator took {0}ms.", operatorCtorMS);
Console.WriteLine("The 'same' -= operator took {0}ms.", operatorNoCtorMS);
Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS);
Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS);
// Compare speeds
long percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
operatorCtorMS -= doNothingMS;
operatorNoCtorMS -= doNothingMS;
plusEqualMS -= doNothingMS;
plusEqualDDMS -= doNothingMS;
Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List...");
percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
}
}
}
Die IL: (aka. Worin einige der oben genannten kompiliert werden)
public void PlusEqual(Element that)
{
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,30h
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
00000013 mov dword ptr [ebp-3Ch],ecx
00000016 cmp dword ptr ds:[04C87B7Ch],0
0000001d je 00000024
0000001f call 753081B1
00000024 nop
this.Left += that.Left;
00000025 mov eax,dword ptr [ebp-3Ch]
00000028 fld qword ptr [ebp+8]
0000002b fadd qword ptr [eax]
0000002d fstp qword ptr [eax]
this.Right += that.Right;
0000002f mov eax,dword ptr [ebp-3Ch]
00000032 fld qword ptr [ebp+10h]
00000035 fadd qword ptr [eax+8]
00000038 fstp qword ptr [eax+8]
}
0000003b nop
0000003c lea esp,[ebp-0Ch]
0000003f pop ebx
00000040 pop esi
00000041 pop edi
00000042 pop ebp
00000043 ret 10h
public void PlusEqual(double thatLeft, double thatRight)
{
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,30h
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
00000013 mov dword ptr [ebp-3Ch],ecx
00000016 cmp dword ptr ds:[04C87B7Ch],0
0000001d je 00000024
0000001f call 75308159
00000024 nop
this.Left += thatLeft;
00000025 mov eax,dword ptr [ebp-3Ch]
00000028 fld qword ptr [ebp+10h]
0000002b fadd qword ptr [eax]
0000002d fstp qword ptr [eax]
this.Right += thatRight;
0000002f mov eax,dword ptr [ebp-3Ch]
00000032 fld qword ptr [ebp+8]
00000035 fadd qword ptr [eax+8]
00000038 fstp qword ptr [eax+8]
}
0000003b nop
0000003c lea esp,[ebp-0Ch]
0000003f pop ebx
00000040 pop esi
00000041 pop edi
00000042 pop ebp
00000043 ret 10h
quelle
Antworten:
Ich bekomme sehr unterschiedliche Ergebnisse, viel weniger dramatisch. Aber ich habe den Test Runner nicht verwendet, sondern den Code in eine Konsolenmodus-App eingefügt. Das 5% -Ergebnis ist ~ 87% im 32-Bit-Modus, ~ 100% im 64-Bit-Modus, wenn ich es versuche.
Die Ausrichtung ist bei Doppelwerten von entscheidender Bedeutung. Die .NET-Laufzeit kann auf einem 32-Bit-Computer nur eine Ausrichtung von 4 versprechen. Für mich startet der Testläufer die Testmethoden mit einer Stapeladresse, die auf 4 statt auf 8 ausgerichtet ist. Die Strafe für die Fehlausrichtung wird sehr groß, wenn das Double eine Cache-Zeilengrenze überschreitet.
quelle
Ich habe einige Schwierigkeiten, Ihre Ergebnisse zu replizieren.
Ich habe deinen Code genommen:
Als ich das tat, bekam ich die folgenden Timings, die sich stark von Ihren unterscheiden. Um Zweifel zu vermeiden, werde ich genau den Code veröffentlichen, den ich verwendet habe.
Hier sind meine Timings
Und das sind meine Änderungen an Ihrem Code:
quelle
Hier wird .NET 4.0 ausgeführt. Ich habe mit "Any CPU" kompiliert und auf .NET 4.0 im Release-Modus abgezielt. Die Ausführung erfolgte über die Befehlszeile. Es lief im 64-Bit-Modus. Meine Timings sind etwas anders.
Insbesondere
PlusEqual(Element)
ist etwas schneller alsPlusEqual(double, double)
.Was auch immer das Problem in .NET 3.5 ist, es scheint in .NET 4.0 nicht zu existieren.
quelle
Wie @Corey Kosak habe ich diesen Code gerade in VS 2010 Express als einfache Konsolen-App im Release-Modus ausgeführt. Ich bekomme sehr unterschiedliche Zahlen. Aber ich habe auch Fx4.5, so dass dies möglicherweise nicht das Ergebnis für eine saubere Fx4.0 ist.
Bearbeiten: und jetzt von der cmd-Zeile ausführen. Das macht einen Unterschied und weniger Variationen in den Zahlen.
quelle
Zusätzlich zu den in anderen Antworten erwähnten JIT-Compiler-Unterschieden besteht ein weiterer Unterschied zwischen einem Strukturmethodenaufruf und einem Strukturoperator darin, dass ein Strukturmethodenaufruf
this
alsref
Parameter übergeben wird (und geschrieben werden kann, um auch andere Parameter alsref
Parameter zu akzeptieren ), während a Der struct-Operator übergibt alle Operanden als Wert. Die Kosten für die Übergabe einer Struktur beliebiger Größe alsref
Parameter sind fest, unabhängig davon, wie groß die Struktur ist, während die Kosten für die Übergabe größerer Strukturen proportional zur Strukturgröße sind. Es ist nichts Falsches daran, große Strukturen (sogar Hunderte von Bytes) zu verwenden, wenn man vermeiden kann, sie unnötig zu kopieren . Während unnötige Kopien bei der Verwendung von Methoden häufig verhindert werden können, können sie bei der Verwendung von Operatoren nicht verhindert werden.quelle
Ich bin mir nicht sicher, ob dies relevant ist, aber hier sind die Zahlen für .NET 4.0 64-Bit unter Windows 7 64-Bit. Meine mscorwks.dll-Version ist 2.0.50727.5446. Ich habe den Code einfach in LINQPad eingefügt und von dort aus ausgeführt. Hier ist das Ergebnis:
quelle
Ich würde mir vorstellen, dass beim Zugriff auf Mitglieder der Struktur tatsächlich eine zusätzliche Operation ausgeführt wird, um auf das Mitglied zuzugreifen, der DIESE Zeiger + Offset.
quelle
Vielleicht sollten Sie anstelle von List double [] mit "bekannten" Offsets und Indexinkrementen verwenden?
quelle