Dlaczego operatorzy są o wiele wolniejsi niż wywołania metod? (struktury są wolniejsze tylko w starszych JIT)

84

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 
Brian Kennedy
źródło
22
Wow, należy to przywołać jako przykład tego, jak może wyglądać dobre pytanie dotyczące Stackoverflow! Można pominąć tylko komentarze wygenerowane automatycznie. Niestety wiem za mało, aby wniknąć w ten problem, ale bardzo podoba mi się to pytanie!
Dennis Traub,
2
Nie sądzę, aby test jednostkowy był dobrym miejscem do przeprowadzenia testu porównawczego.
Henk Holterman
1
Dlaczego struktura musi być szybsza od dwóch podwójnych? W .NET struktura NIGDY nie jest równa sumie rozmiarów jej elementów członkowskich. Więc z definicji jest większy, więc z definicji musi być wolniejszy przy pushowaniu na stosie, niż tylko 2 podwójne wartości. Jeśli kompilator wstawi parametr struct w wierszu 2 do podwójnej pamięci, co jeśli w metodzie chcesz uzyskać dostęp do tej struktury za pomocą odbicia. Gdzie będą znajdować się informacje o środowisku wykonawczym połączone z tym obiektem struktury? Czy to nie jest, czy coś mi brakuje?
Tigran,
3
@Tigran: Potrzebujesz źródeł dla tych twierdzeń. Myślę, że jesteś w błędzie. Tylko wtedy, gdy typ wartości zostanie umieszczony w ramce, metadane muszą być przechowywane z wartością. W zmiennej o statycznym typie struktury nie ma narzutu.
Ben Voigt,
1
Myślałem, że jedyne, czego brakuje, to montaż. A teraz to dodałeś (proszę zauważyć, że jest to asembler x86, a NIE MSIL).
Ben Voigt

Odpowiedzi:

9

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.

Hans Passant
źródło
Dlaczego .NET może zasadniczo odnieść sukces przy dopasowaniu tylko 4 podwójnych? Wyrównanie jest wykonywane przy użyciu 4-bajtowych fragmentów na komputerze 32-bitowym. Jaki jest problem?
Tigran
Dlaczego środowisko wykonawcze dopasowuje się tylko do 4 bajtów na x86? Myślę, że może to wyrównać do 64 bitów, jeśli będzie wymagało dodatkowej uwagi, gdy niezarządzany kod wywołuje kod zarządzany. Chociaż specyfikacja ma tylko słabe gwarancje wyrównania, implementacje powinny być w stanie dostosować się ściślej. (Specyfikacja: „8-bajtowe dane są prawidłowo wyrównane, gdy są przechowywane na tej samej granicy wymaganej przez podstawowy sprzęt do atomowego dostępu do natywnego int”)
CodesInChaos
1
@Code - Cóż, mogłoby to zrobić, generatory kodu C robią to, wykonując obliczenia matematyczne na wskaźniku stosu w prologu funkcji. Jitter x86 po prostu nie. Jest to o wiele ważniejsze w przypadku języków natywnych, ponieważ alokowanie tablic na stosie jest znacznie bardziej powszechne i mają one alokator sterty, który jest wyrównany do 8, więc nigdy nie chcieliby, aby alokacje stosu były mniej wydajne niż alokacje sterty. Utknęliśmy z wyrównaniem 4 z 32-bitowej sterty gc.
Hans Passant
5

Mam trudności z odtworzeniem twoich wyników.

Wziąłem twój kod:

  • uczynił z niego samodzielną aplikację konsolową
  • zbudował zoptymalizowaną (wydaną) kompilację
  • zwiększono współczynnik „rozmiaru” z 2,5 mln do 10 mln
  • uruchomiłem go z wiersza poleceń (poza IDE)

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);
    }
  }
}
Corey Kosak
źródło
Właśnie zrobiłem to samo, moje wyniki są bardziej podobne do twoich. Proszę podać platformę i typ CPu.
Henk Holterman
Bardzo interesujące! Poprosiłem innych, aby zweryfikowali moje wyniki ... jesteś pierwszy, który się wyróżnia. Pierwsze pytanie do Ciebie: jaki jest numer wersji pliku, o którym wspominam w moim poście ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... to ten, który wskazywał dokumenty Microsoft posiadaną wersję JIT Optimizer. (Jeśli mogę po prostu powiedzieć swoim użytkownikom, aby zaktualizowali .NET, aby zobaczyć duże przyspieszenia, będę szczęśliwym kamperem. Ale zgaduję, że to nie będzie takie proste.)
Brian Kennedy,
Pracowałem w programie Visual Studio ... działającym pod Windows XP SP3 ... na maszynie wirtualnej VMware ... z procesorem Intel Core i7 2,7 GHz. Ale to nie czasy absolutne mnie interesują ... to proporcje ... Spodziewałbym się, że te trzy metody będą działać podobnie, co zrobili dla Corey, ale NIE dla mnie.
Brian Kennedy
Moje właściwości projektu to: Konfiguracja: Wydanie; Platforma: aktywna (x86); Cel platformy: x86
Corey Kosak
1
Jeśli chodzi o twoją prośbę o pobranie wersji mscorwks ... Przepraszam, czy chcesz, żebym uruchomił to na .NET 2.0? Moje testy były na .NET 4.0
Corey Kosak
3

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.

Jim Mischel
źródło
2
Tak, wydaje się, że odpowiedź w Structs brzmi „pobierz nowszy JIT”. Ale jak zapytałem o odpowiedź Henka, dlaczego metody są o wiele szybsze niż operatorzy? Obie metody są 5x szybsze niż którykolwiek z operatorów ... który robi dokładnie to samo. To wspaniale, że mogę znowu używać struktur ... ale smutne, że wciąż muszę unikać operatorów.
Brian Kennedy
Jim, bardzo chciałbym poznać wersję pliku C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll w twoim systemie ... jeśli jest nowsza niż moja (.3620), ale starsza niż Corey (.5446), to może wyjaśniać, dlaczego twoi operatorzy są nadal powolni jak moje, ale Corey nie.
Brian Kennedy
@Brian: Wersja pliku 2.0.50727.4214.
Jim Mischel,
DZIĘKI! Muszę więc upewnić się, że moi użytkownicy mają 4214 lub nowszy, aby uzyskać optymalizacje struktury i 5446 lub nowszy, aby uzyskać optymalizację operatora. Muszę dodać kod, żeby to sprawdzić przy starcie i dać ostrzeżenia. Dzięki jeszcze raz.
Brian Kennedy,
2

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.

Henk Holterman
źródło
Tak, wygląda na to, że późniejszy JIT rozwiązał problem ze strukturą, ale pozostaje moje pytanie, dlaczego metody są o wiele szybsze niż operatorzy. Zobacz, o ile szybsze są obie metody PlusEqual niż równoważny operator + =. Interesujące jest również to, o ile szybciej - = jest niż + = ... Twoje czasy są pierwszym, w którym to widziałem.
Brian Kennedy,
Henk, bardzo chciałbym poznać wersję pliku C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll w twoim systemie ... jeśli jest nowsza niż moja (.3620), ale starsza niż Corey (.5446), to może wyjaśniać, dlaczego twoi operatorzy są nadal powolni jak moje, ale Corey nie.
Brian Kennedy,
1
Mogę znaleźć tylko wersję .50727, ale nie jestem pewien, czy jest to istotne dla Fx40 / Fx45?
Henk Holterman,
Musisz przejść do Właściwości i kliknąć kartę Wersja, aby zobaczyć resztę numeru wersji.
Brian Kennedy,
2

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 thisjako refparametr (i może zostać zapisane tak, aby akceptowało również inne parametry jako refparametry), podczas gdy struct przekaże wszystkie operandy według wartości. Koszt przekazania struktury o dowolnym rozmiarze jako refparametru 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.

supercat
źródło
Hmmm ... cóż, to mogłoby wiele wyjaśnić! Tak więc, jeśli operator jest na tyle krótki, że będzie wstawiony, zakładam, że nie zrobi niepotrzebnych kopii. Ale jeśli nie, a twoja struktura składa się z więcej niż jednego słowa, możesz nie chcieć implementować jej jako operatora, jeśli szybkość ma kluczowe znaczenie. Dzięki za ten wgląd.
Brian Kennedy
Przy okazji, jedna rzecz, która mnie trochę denerwuje, gdy odpowiedzi na pytania dotyczące szybkości są „test porównawczy!” jest taka, że ​​taka odpowiedź ignoruje fakt, że w wielu przypadkach liczy się to, czy operacja zwykle zajmuje 10us czy 20us, ale czy niewielka zmiana okoliczności może spowodować, że zajmie jej 1ms czy 10ms. Liczy się nie to, jak szybko coś działa na komputerze dewelopera, ale raczej to, czy operacja kiedykolwiek będzie wystarczająco wolna, aby miała znaczenie ; jeśli metoda X działa dwa razy szybciej niż metoda Y na większości maszyn, ale na niektórych komputerach będzie 100 razy wolniejsza, metoda Y może być lepszym wyborem.
supercat
Oczywiście mówimy tutaj o zaledwie 2 podwójnych ... nie dużych strukturach. Przekazanie dwóch dubletów na stosie, gdzie można do nich szybko uzyskać dostęp, niekoniecznie jest wolniejsze niż przekazanie „tego” na stosie, a następnie konieczność wyodrębnienia tego, aby przyciągnąć je do operacji na nich… ale może to spowodować różnice. Jednak w tym przypadku powinien on być wstawiony, więc optymalizator JIT powinien otrzymać dokładnie ten sam kod.
Brian Kennedy,
1

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%.
Daniel Pryden
źródło
2
Interesujące ... wydawałoby się, że optymalizacje, które zostały dodane do 32b JIT Optimizer nie dotarły jeszcze do 64b JIT Optimizer ... Twoje współczynniki są nadal bardzo podobne do moich. Rozczarowujące ... ale dobrze wiedzieć.
Brian Kennedy
0

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.

Mateusz
źródło
1
Cóż, w przypadku obiektu klasy miałbyś absolutną rację ... ponieważ do metody zostałby po prostu przekazany wskaźnik „this”. Jednak w przypadku struktur nie powinno tak być. Struct należy przekazać do metod na stosie. Tak więc, pierwsza podwójna powinna znajdować się tam, gdzie powinien znajdować się wskaźnik „ten”, a druga podwójna w pozycji tuż za nią ... oba prawdopodobnie są rejestrowane w procesorze. Tak więc JIT powinien używać co najwyżej przesunięcia.
Brian Kennedy
0

Może zamiast List powinieneś użyć double [] z "dobrze znanymi" przesunięciami i przyrostami indeksu?

Konstantin Isaev
źródło