Intro: Piszę kod o wysokiej wydajności w C #. Tak, wiem, że C ++ dałoby mi lepszą optymalizację, ale nadal wybieram C #. Nie chcę debatować nad tym wyborem. Raczej chciałbym usłyszeć od tych, którzy, tak jak ja, próbują napisać kod o wysokiej wydajności w .NET Framework.
Pytania:
- Dlaczego operator w poniższym kodzie jest wolniejszy niż równoważne wywołanie metody?
- Dlaczego metoda przekazująca dwa podwojenia w poniższym kodzie jest szybsza niż równoważna metoda przekazująca strukturę, która ma dwa podwojenia w środku? (A: starsze JIT źle optymalizują struktury)
- Czy istnieje sposób, aby kompilator .NET JIT traktował proste struktury tak wydajnie, jak elementy składowe struktury? (A: pobierz nowszy JIT)
Myślę, że wiem: oryginalny kompilator .NET JIT nie zawierałby niczego, co dotyczyło struktury. Dziwacznie podane struktury powinny być używane tylko wtedy, gdy potrzebujesz małych typów wartości, które powinny być zoptymalizowane jak wbudowane, ale prawdziwe. Na szczęście w .NET 3.5SP1 i .NET 2.0SP2 wprowadzili pewne ulepszenia do JIT Optimizer, w tym ulepszenia dotyczące wstawiania, szczególnie dla struktur. (Domyślam się, że zrobili to, ponieważ w przeciwnym razie nowa struktura Complex, którą wprowadzali, działałaby okropnie ... więc zespół Complex prawdopodobnie naciskał na zespół JIT Optimizer.) Tak więc każda dokumentacja przed .NET 3.5 SP1 jest prawdopodobnie niezbyt istotne w tej kwestii.
Co pokazują moje testy: Po potwierdzeniu, że mam nowszy JIT Optimizer, sprawdziłem, czy plik C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ma wersję> = 3053 i dlatego powinienem mieć te ulepszenia do optymalizatora JIT. Jednak nawet z tym, jakie moje czasy i wyglądają przy demontażu oba pokazują:
Kod utworzony przez JIT do przekazywania struktury z dwoma podwójnymi jest znacznie mniej wydajny niż kod, który bezpośrednio przekazuje dwa podwójne.
Kod utworzony przez JIT dla metody struct przekazuje 'this' znacznie wydajniej niż w przypadku przekazania struktury jako argumentu.
JIT nadal działa lepiej, jeśli zdasz dwa podwójne, zamiast przekazać strukturę z dwoma podwójnymi, nawet z mnożnikiem, ponieważ jest wyraźnie w pętli.
Czasy: Właściwie, patrząc na demontaż, zdaję sobie sprawę, że większość czasu w pętlach to po prostu dostęp do danych testowych z listy. Różnica między czterema sposobami wykonywania tych samych wywołań jest diametralnie różna, jeśli weźmie się pod uwagę kod narzutu pętli i dostęp do danych. Dostaję od 5x do 20x przyspieszenia za zrobienie PlusEqual (podwójne, podwójne) zamiast PlusEqual (Element). Oraz 10x do 40x za wykonanie PlusEqual (double, double) zamiast operatora + =. Łał. Smutny.
Oto jeden zestaw czasów:
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%.
Kod:
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);
}
}
}
IL: (aka. Do czego zostały wkompilowane niektóre z powyższych)
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
źródło
Odpowiedzi:
Otrzymuję bardzo różne wyniki, znacznie mniej dramatyczne. Ale nie korzystałem z programu uruchamiającego testy, wkleiłem kod do aplikacji w trybie konsoli. Wynik 5% to ~ 87% w trybie 32-bitowym, ~ 100% w trybie 64-bitowym, kiedy próbuję.
Wyrównanie jest krytyczne w przypadku gier podwójnych, a środowisko wykonawcze .NET może zapewnić wyrównanie tylko 4 na komputerze 32-bitowym. Wygląda na to, że biegacz testów uruchamia metody testowe z adresem stosu, który jest wyrównany do 4 zamiast 8. Kara za niewspółosiowość staje się bardzo duża, gdy podwójny przekracza granicę linii pamięci podręcznej.
źródło
Mam trudności z odtworzeniem twoich wyników.
Wziąłem twój kod:
Kiedy to zrobiłem, otrzymałem następujące czasy, które znacznie różnią się od twoich. Aby uniknąć wątpliwości, prześlę dokładnie kod, którego użyłem.
Oto moje czasy
Populating List<Element> took 527ms. The PlusEqual() method took 450ms. The 'same' += operator took 386ms. The 'same' -= operator took 446ms. The PlusEqual(double, double) method took 413ms. The do nothing loop took 229ms. The ratio of operator with constructor to method is 85%. The ratio of operator without constructor to method is 99%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 91%. If we remove the overhead time for the loop accessing the elements from the List... The ratio of operator with constructor to method is 71%. The ratio of operator without constructor to method is 98%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 83%.
A oto moje zmiany w Twoim kodzie:
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; } } public class UnitTest1 { public static void Main() { 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 = 10000000 * 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 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); } } }
źródło
Uruchomiony .NET 4.0 tutaj. Skompilowałem z "Any CPU", nastawionym na .NET 4.0 w trybie wydania. Wykonywanie było z wiersza poleceń. Działał w trybie 64-bitowym. Moje czasy są nieco inne.
Populating List<Element> took 442ms. The PlusEqual() method took 115ms. The 'same' += operator took 201ms. The 'same' -= operator took 200ms. The PlusEqual(double, double) method took 129ms. The do nothing loop took 93ms. The ratio of operator with constructor to method is 174%. The ratio of operator without constructor to method is 173%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 112%. If we remove the overhead time for the loop accessing the elements from the List ... The ratio of operator with constructor to method is 490%. The ratio of operator without constructor to method is 486%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 163%.
W szczególności
PlusEqual(Element)
jest nieco szybszy niżPlusEqual(double, double)
.Jakikolwiek problem występuje w .NET 3.5, wydaje się, że nie istnieje w .NET 4.0.
źródło
Podobnie jak @Corey Kosak, właśnie uruchomiłem ten kod w VS 2010 Express jako prostą aplikację konsolową w trybie wydania. Otrzymuję bardzo różne liczby. Ale mam też Fx4,5, więc mogą to nie być wyniki dla czystego Fx4.0.
Populating List<Element> took 435ms. The PlusEqual() method took 109ms. The 'same' += operator took 217ms. The 'same' -= operator took 157ms. The PlusEqual(double, double) method took 118ms. The do nothing loop took 79ms. The ratio of operator with constructor to method is 199%. The ratio of operator without constructor to method is 144%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 108%. If we remove the overhead time for the loop accessing the elements from the List ... The ratio of operator with constructor to method is 460%. The ratio of operator without constructor to method is 260%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 130%.
Edytuj: a teraz uruchom z linii cmd. To robi różnicę i mniejsze różnice w liczbach.
źródło
Oprócz różnic w kompilatorze JIT wspomnianych w innych odpowiedziach, inną różnicą między wywołaniem metody struct a operatorem struct jest to, że wywołanie metody struct zostanie przekazane
this
jakoref
parametr (i może zostać zapisane tak, aby akceptowało również inne parametry jakoref
parametry), podczas gdy struct przekaże wszystkie operandy według wartości. Koszt przekazania struktury o dowolnym rozmiarze jakoref
parametru jest stały, bez względu na to, jak duża jest struktura, podczas gdy koszt przekazania większych struktur jest proporcjonalny do rozmiaru struktury. Nie ma nic złego w używaniu dużych struktur (nawet setek bajtów), jeśli można uniknąć ich niepotrzebnego kopiowania ; Podczas gdy niepotrzebnym kopiom można często zapobiec stosując metody, nie można im zapobiec używając operatorów.źródło
Nie jestem pewien, czy to ma znaczenie, ale oto liczby dotyczące 64-bitowego .NET 4.0 w 64-bitowym systemie Windows 7. Moja wersja mscorwks.dll to 2.0.50727.5446. Właśnie wkleiłem kod do LINQPad i uruchomiłem go stamtąd. Oto wynik:
Populating List<Element> took 496ms. The PlusEqual() method took 189ms. The 'same' += operator took 295ms. The 'same' -= operator took 358ms. The PlusEqual(double, double) method took 148ms. The do nothing loop took 103ms. The ratio of operator with constructor to method is 156%. The ratio of operator without constructor to method is 189%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 78%. If we remove the overhead time for the loop accessing the elements from the List ... The ratio of operator with constructor to method is 223%. The ratio of operator without constructor to method is 296%. The ratio of PlusEqual(double,double) to PlusEqual(Element) is 52%.
źródło
Wyobrażam sobie, że gdy uzyskujesz dostęp do elementów struktury, faktycznie wykonuje dodatkową operację, aby uzyskać dostęp do elementu, TEN wskaźnik + przesunięcie.
źródło
Może zamiast List powinieneś użyć double [] z "dobrze znanymi" przesunięciami i przyrostami indeksu?
źródło