Warum sind Operatoren so viel langsamer als Methodenaufrufe? (Strukturen sind nur bei älteren JITs langsamer)

84

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 
Brian Kennedy
quelle
22
Wow, dies sollte als Beispiel dafür dienen, wie eine gute Frage zu Stackoverflow aussehen kann! Nur die automatisch generierten Kommentare konnten weggelassen werden. Leider weiß ich zu wenig, um mich wirklich mit dem Problem zu befassen, aber die Frage gefällt mir wirklich!
Dennis Traub
2
Ich denke nicht, dass ein Unit Test ein guter Ort ist, um einen Benchmark durchzuführen.
Henk Holterman
1
Warum muss die Struktur schneller sein als zwei Doppel? In .NET ist struct NIEMALS gleich der Summe der Größen seiner Mitglieder. Per Definition ist es also größer, also muss es per Definition beim Drücken auf den Stapel langsamer sein als nur 2 Doppelwerte. Wenn der Compiler den Strukturparameter im doppelten Speicher von Zeile 2 einbindet, was ist, wenn Sie innerhalb der Methode mit Reflektion auf diese Struktur zugreifen möchten? Wo befinden sich Laufzeitinformationen, die mit diesem Strukturobjekt verknüpft sind? Ist es nicht so oder fehlt mir etwas?
Tigran
3
@Tigran: Sie benötigen Quellen für diese Behauptungen. Ich denke du liegst falsch. Nur wenn ein Werttyp eingerahmt wird, müssen Metadaten mit dem Wert gespeichert werden. In einer Variablen mit statischem Strukturtyp entsteht kein Overhead.
Ben Voigt
1
Ich dachte, dass das einzige, was fehlte, die Versammlung war. Und jetzt haben Sie das hinzugefügt (bitte beachten Sie, dass dies x86-Assembler und NICHT MSIL ist).
Ben Voigt

Antworten:

9

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.

Hans Passant
quelle
Warum kann .NET grundsätzlich erfolgreich sein, wenn nur 4 Doppel ausgerichtet werden? Die Ausrichtung erfolgt mithilfe von 4-Byte-Blöcken auf einem 32-Bit-Computer. Was ist dort ein Problem?
Tigran
Warum wird die Laufzeit auf x86 nur auf 4 Byte ausgerichtet? Ich denke, es könnte auf 64-Bit ausgerichtet werden, wenn zusätzliche Sorgfalt angewendet wird, wenn nicht verwalteter Code verwalteten Code aufruft. Während die Spezifikation nur schwache Ausrichtungsgarantien aufweist, sollten die Implementierungen in der Lage sein, strenger auszurichten. (Spezifikation: "8-Byte-Daten werden ordnungsgemäß ausgerichtet, wenn sie an derselben Grenze gespeichert sind, die von der zugrunde liegenden Hardware für den atomaren Zugriff auf ein natives int benötigt wird")
CodesInChaos
1
@Code - Nun, es könnte sein, dass C-Code-Generatoren dies tun, indem sie den Stapelzeiger im Funktionsprolog berechnen. Der x86-Jitter tut es einfach nicht. Dies ist für Muttersprachen viel wichtiger, da das Zuweisen von Arrays auf dem Stapel viel häufiger ist und sie einen Heap-Allokator haben, der auf 8 ausgerichtet ist. Daher möchten sie die Stapelzuweisungen niemals weniger effizient machen als die Heap-Zuweisungen. Wir stecken mit einer Ausrichtung von 4 aus dem 32-Bit-GC-Heap fest.
Hans Passant
5

Ich habe einige Schwierigkeiten, Ihre Ergebnisse zu replizieren.

Ich habe deinen Code genommen:

  • machte es zu einer eigenständigen Konsolenanwendung
  • hat einen optimierten (Release-) Build erstellt
  • erhöhte den "Größen" -Faktor von 2,5 M auf 10 M.
  • lief es von der Kommandozeile (außerhalb der IDE)

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

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%.

Und das sind meine Änderungen an Ihrem 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;
    }    
  }    

  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
quelle
Ich habe genau das Gleiche getan, meine Ergebnisse ähneln eher Ihren. Bitte geben Sie die Plattform und den CPu-Typ an.
Henk Holterman
Sehr interessant! Ich habe andere meine Ergebnisse überprüfen lassen ... Sie sind die Ersten, die anders werden. Erste Frage an Sie: Wie lautet die Versionsnummer der Datei, die ich in meinem Beitrag erwähne ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... das ist die in den Microsoft-Dokumenten angegebene die Version von JIT Optimizer, die Sie haben. (Wenn ich meinen Benutzern nur sagen kann, dass sie ihr .NET aktualisieren sollen, um große Beschleunigungen zu sehen, bin ich ein glücklicher Camper. Aber ich vermute, dass es nicht so einfach sein wird.)
Brian Kennedy
Ich habe in Visual Studio ausgeführt ... unter Windows XP SP3 ... in einer virtuellen VMware-Maschine ... auf einem 2,7-GHz-Intel Core i7. Aber es sind nicht die absoluten Zeiten, die mich interessieren ... es sind die Verhältnisse ... Ich würde erwarten, dass diese drei Methoden alle ähnlich funktionieren, was sie für Corey getan haben, aber NICHT für mich.
Brian Kennedy
Meine Projekteigenschaften sagen: Konfiguration: Release; Plattform: Aktiv (x86); Plattformziel: x86
Corey Kosak
1
In Bezug auf Ihre Anfrage, die Version von mscorwks zu erhalten ... Entschuldigung, wollten Sie, dass ich dieses Ding gegen .NET 2.0 ausführe? Meine Tests waren auf .NET 4.0
Corey Kosak
3

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.

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%.

Insbesondere PlusEqual(Element)ist etwas schneller als PlusEqual(double, double).

Was auch immer das Problem in .NET 3.5 ist, es scheint in .NET 4.0 nicht zu existieren.

Jim Mischel
quelle
2
Ja, die Antwort auf Structs scheint "Holen Sie sich die neuere JIT". Aber wie ich auf Henks Antwort gefragt habe, warum sind Methoden so viel schneller als Operatoren? Beide Methoden sind 5x schneller als jeder Ihrer Operatoren ... die genau das Gleiche tun. Es ist toll, dass ich wieder Strukturen verwenden kann ... aber traurig, dass ich noch Operatoren meiden muss.
Brian Kennedy
Jim, ich wäre sehr interessiert, die Version der Datei C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll auf Ihrem System zu kennen ... falls neuer als meine (.3620), aber älter als Corey's (.5446) könnte dies erklären, warum Ihre Operatoren immer noch langsam sind wie meine, Corey's jedoch nicht.
Brian Kennedy
@Brian: Dateiversion 2.0.50727.4214.
Jim Mischel
VIELEN DANK! Daher muss ich sicherstellen, dass meine Benutzer 4214 oder höher haben, um Strukturoptimierungen zu erhalten, und 5446 oder höher, um die Operatoroptimierung zu erhalten. Ich muss Code hinzufügen, um dies beim Start zu überprüfen und einige Warnungen zu geben. Danke noch einmal.
Brian Kennedy
2

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.

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%.

Bearbeiten: und jetzt von der cmd-Zeile ausführen. Das macht einen Unterschied und weniger Variationen in den Zahlen.

Henk Holterman
quelle
Ja, es scheint, dass die spätere JIT das Strukturproblem behoben hat, aber meine Frage, warum Methoden so viel schneller als Operatoren sind, bleibt offen. Sehen Sie, wie viel schneller beide PlusEqual-Methoden sind als der entsprechende Operator + =. Und es ist auch interessant, wie viel schneller - = ist als + = ... Ihre Timings sind die ersten, bei denen ich das gesehen habe.
Brian Kennedy
Henk, ich wäre sehr interessiert, die Version der Datei C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll auf Ihrem System zu kennen ... wenn neuer als meine (.3620), aber älter als Corey's (.5446) könnte dies erklären, warum Ihre Operatoren immer noch langsam sind wie meine, Corey's jedoch nicht.
Brian Kennedy
1
Ich kann nur die .50727-Version finden, bin mir aber nicht sicher, ob dies für Fx40 / Fx45 relevant ist.
Henk Holterman
Sie müssen in die Eigenschaften gehen und auf die Registerkarte Version klicken, um den Rest der Versionsnummer anzuzeigen.
Brian Kennedy
2

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 thisals refParameter übergeben wird (und geschrieben werden kann, um auch andere Parameter als refParameter zu akzeptieren ), während a Der struct-Operator übergibt alle Operanden als Wert. Die Kosten für die Übergabe einer Struktur beliebiger Größe als refParameter 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.

Superkatze
quelle
Hmmm ... nun, das könnte viel erklären! Wenn der Operator also kurz genug ist, um eingebunden zu werden, werden vermutlich keine unnötigen Kopien erstellt. Wenn dies nicht der Fall ist und Ihre Struktur aus mehr als einem Wort besteht, möchten Sie sie möglicherweise nicht als Operator implementieren, wenn die Geschwindigkeit kritisch ist. Danke für diesen Einblick.
Brian Kennedy
Übrigens, eine Sache, die mich ein wenig nervt, wenn Fragen zur Geschwindigkeit beantwortet werden "Benchmark it!" ist, dass eine solche Antwort die Tatsache ignoriert, dass es in vielen Fällen darauf ankommt, ob eine Operation normalerweise 10us oder 20us dauert, aber ob eine geringfügige Änderung der Umstände dazu führen kann, dass sie 1 ms oder 10 ms dauert. Was zählt, ist nicht, wie schnell etwas auf dem Computer eines Entwicklers läuft, sondern ob der Vorgang jemals langsam genug sein wird, um eine Rolle zu spielen . Wenn Methode X auf den meisten Computern doppelt so schnell wie Methode Y ausgeführt wird, auf einigen Computern jedoch 100-mal so langsam ist, ist Methode Y möglicherweise die bessere Wahl.
Supercat
Natürlich sprechen wir hier nur von 2 Doppel ... nicht von großen Strukturen. Das Übergeben von zwei Doubles auf dem Stapel, auf die schnell zugegriffen werden kann, ist nicht unbedingt langsamer als das Übergeben von "this" auf dem Stapel und das anschließende Dereferenzieren, um sie einzuziehen, um sie zu bearbeiten. Dies kann jedoch zu Unterschieden führen. In diesem Fall sollte es jedoch inline sein, damit der JIT-Optimierer genau denselben Code erhält.
Brian Kennedy
1

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:

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
quelle
2
Interessant ... es scheint, dass die Optimierungen, die dem 32b JIT Optimizer hinzugefügt wurden, es noch nicht zum 64b JIT Optimizer geschafft haben ... Ihre Verhältnisse sind meinen immer noch sehr ähnlich. Enttäuschend ... aber gut zu wissen.
Brian Kennedy
0

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.

Matthew
quelle
1
Nun, mit einem Klassenobjekt hätten Sie absolut Recht ... weil der Methode nur der 'this'-Zeiger übergeben würde. Bei Strukturen sollte dies jedoch nicht so sein. Die Struktur sollte an die Methoden auf dem Stapel übergeben werden. Das erste Double sollte also dort sitzen, wo sich der 'this'-Zeiger befinden würde, und das zweite Double an der Position direkt danach ... beide sind möglicherweise Register in der CPU. Die JIT sollte also höchstens einen Offset verwenden.
Brian Kennedy
0

Vielleicht sollten Sie anstelle von List double [] mit "bekannten" Offsets und Indexinkrementen verwenden?

Konstantin Isaev
quelle