Perché gli operatori sono molto più lenti delle chiamate ai metodi? (le strutture sono più lente solo su JIT meno recenti)


84

Intro: scrivo codice ad alte prestazioni in C #. Sì, so che C ++ mi darebbe una migliore ottimizzazione, ma scelgo comunque di usare C #. Non desidero discutere questa scelta. Piuttosto, mi piacerebbe sentire chi, come me, sta cercando di scrivere codice ad alte prestazioni su .NET Framework.

Domande:

  • Perché l'operatore nel codice seguente è più lento della chiamata al metodo equivalente ??
  • Perché il metodo che passa due double nel codice sottostante è più veloce del metodo equivalente che passa una struttura che ha due double all'interno? (A: JIT più vecchi ottimizzano male le strutture)
  • C'è un modo per fare in modo che il compilatore .NET JIT tratti le strutture semplici in modo efficiente quanto i membri della struttura? (A: ottieni il JIT più recente)

Quello che penso di sapere: il compilatore .NET JIT originale non integrava nulla che implicasse una struttura. Gli struct dati bizzarri dovrebbero essere usati solo dove hai bisogno di tipi di valore piccolo che dovrebbero essere ottimizzati come built-in, ma vero. Fortunatamente, in .NET 3.5SP1 e .NET 2.0SP2, sono stati apportati alcuni miglioramenti all'ottimizzatore JIT, inclusi miglioramenti all'inlining, in particolare per le strutture. (Immagino che lo abbiano fatto perché altrimenti la nuova struttura Complex che stavano introducendo avrebbe funzionato in modo orribile ... quindi il team Complex probabilmente stava martellando sul team di JIT Optimizer.) Quindi, qualsiasi documentazione precedente a .NET 3.5 SP1 è probabilmente non troppo rilevante per questo problema.

Cosa mostra il mio test: ho verificato di avere il più recente JIT Optimizer controllando che il file C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll abbia la versione> = 3053 e quindi dovrebbero avere quei miglioramenti all'ottimizzatore JIT. Tuttavia, anche con quello, quali sono i miei tempi e guarda allo smontaggio entrambi mostrano:

Il codice prodotto da JIT per passare una struttura con due doppi è molto meno efficiente del codice che passa direttamente i due doppi.

Il codice prodotto da JIT per un metodo struct passa "this" in modo molto più efficiente rispetto a quando si passa una struct come argomento.

Il JIT è ancora in linea meglio se si passano due doppi anziché una struttura con due doppi, anche con il moltiplicatore perché è chiaramente in un ciclo.

I tempi: In realtà, guardando lo smontaggio, mi rendo conto che la maggior parte delle volte nei cicli è solo l'accesso ai dati di prova fuori dalla lista. La differenza tra i quattro modi per effettuare le stesse chiamate è notevolmente diversa se si tiene conto del codice overhead del ciclo e dell'accesso ai dati. Ottengo velocità da 5x a 20x eseguendo PlusEqual (double, double) invece di PlusEqual (Element). E da 10x a 40x per l'esecuzione di PlusEqual (double, double) invece dell'operatore + =. Wow. Triste.

Ecco una serie di tempi:

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

Il codice:

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);
    }
  }
}

L'IL: (ovvero ciò in cui vengono compilati alcuni dei precedenti)

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 

22
Wow, questo dovrebbe essere un esempio di come può apparire una buona domanda su Stackoverflow! È possibile omettere solo i commenti generati automaticamente. Purtroppo ne so troppo poco per approfondire il problema, ma la domanda mi piace molto!
Dennis Traub

2
Non credo che uno Unit Test sia un buon posto per eseguire un benchmark.
Henk Holterman

1
Perché la struttura deve essere più veloce di due doppie? In .NET struct non è MAI uguale alla somma delle dimensioni dei suoi membri. Quindi, per definizione, è più grande, quindi per definizione deve essere più lento a spingere sullo stack, quindi solo 2 valori doppi. Se il compilatore inserirà il parametro della struttura in linea nella doppia memoria della riga 2, cosa succede se all'interno del metodo si desidera accedere a quella struttura con la riflessione. Dove saranno le informazioni di runtime collegate a quell'oggetto struct? Non è vero, o mi manca qualcosa?
Tigran

3
@Tigran: hai bisogno di fonti per queste affermazioni. Penso che ti sbagli. Solo quando un tipo di valore viene inscatolato, i metadati devono essere archiviati con il valore. In una variabile con tipo struct statico, non c'è overhead.
Ben Voigt

1
Pensavo che l'unica cosa che mancava fosse l'assemblea. E ora l'hai aggiunto (nota che è l'assembler x86 e NON MSIL).
Ben Voigt

Risposte:


9

Ottengo risultati molto diversi, molto meno drammatici. Ma non ho utilizzato il test runner, ho incollato il codice in un'app in modalità console. Il risultato del 5% è ~ 87% in modalità a 32 bit, ~ 100% in modalità a 64 bit quando lo provo.

L'allineamento è fondamentale sui doppi, il runtime .NET può solo promettere un allineamento di 4 su una macchina a 32 bit. Mi sembra che il test runner stia iniziando i metodi di test con un indirizzo di stack allineato a 4 invece di 8. La penalità di disallineamento diventa molto grande quando il doppio attraversa il confine di una linea di cache.


Perché .NET può fondamentalmente avere successo con l'allineamento di soli 4 doppi? L'allineamento viene eseguito utilizzando blocchi di 4 byte su una macchina a 32 bit. Che problema c'è?
Tigran

Perché il runtime si allinea solo a 4 byte su x86? Penso che potrebbe allinearsi a 64 bit se richiede ulteriore attenzione quando il codice non gestito chiama codice gestito. Sebbene le specifiche abbiano solo garanzie di allineamento deboli, le implementazioni dovrebbero essere in grado di allinearsi in modo più rigoroso. (Specifica: "I dati a 8 byte sono allineati correttamente quando vengono memorizzati sullo stesso confine richiesto dall'hardware sottostante per l'accesso atomico a un int nativo")
CodesInChaos

1
@Code - Beh, potrebbe, i generatori di codice C lo fanno eseguendo calcoli sul puntatore dello stack nel prologo della funzione. Il jitter x86 non lo fa. È molto più importante per le lingue native poiché l'allocazione di array nello stack è molto più comune e hanno un allocatore di heap che si allinea a 8, quindi non vorrebbe mai rendere le allocazioni di stack meno efficienti delle allocazioni di heap. Siamo bloccati con un allineamento di 4 dall'heap gc a 32 bit.
Hans Passant

5

Ho qualche difficoltà a replicare i tuoi risultati.

Ho preso il tuo codice:

  • lo ha reso un'applicazione console autonoma
  • costruito un build ottimizzato (rilascio)
  • aumentato il fattore di "dimensione" da 2,5 milioni a 10 milioni
  • eseguito dalla riga di comando (al di fuori dell'IDE)

Quando l'ho fatto, ho ottenuto i seguenti tempi che sono molto diversi dai tuoi. A scanso di equivoci, inserirò esattamente il codice che ho usato.

Ecco i miei tempi

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

E queste sono le mie modifiche al tuo codice:

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);
    }
  }
}

Ho appena fatto lo stesso, i miei risultati sono più simili ai tuoi. Indicare la piattaforma e il tipo di CPu.
Henk Holterman

Molto interessante! Ho chiesto ad altri di verificare i miei risultati ... sei il primo a cambiare. Prima domanda per te: qual è il numero di versione del file che cito nel mio post ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... è quello indicato dai documenti Microsoft la versione di JIT Optimizer che hai. (Se posso solo dire ai miei utenti di aggiornare il loro .NET per vedere grandi accelerazioni, sarò un campeggiatore felice. Ma immagino che non sarà così semplice.)
Brian Kennedy

Stavo eseguendo in Visual Studio ... in esecuzione su Windows XP SP3 ... in una macchina virtuale VMware ... su un Intel Core i7 da 2,7 GHz. Ma non sono i tempi assoluti che mi interessano ... sono i rapporti ... Mi aspetto che questi tre metodi funzionino tutti allo stesso modo, cosa che hanno fatto per Corey, ma NON per me.
Brian Kennedy,

Le proprietà del mio progetto dicono: Configurazione: Rilascio; Piattaforma: attiva (x86); Target della piattaforma: x86
Corey Kosak

1
Per quanto riguarda la tua richiesta di ottenere la versione di mscorwks ... Scusa, volevi che eseguissi questa cosa su .NET 2.0? I miei test erano su .NET 4.0
Corey Kosak

3

Esecuzione di .NET 4.0 qui. Ho compilato con "Any CPU", mirando a .NET 4.0 in modalità di rilascio. L'esecuzione è stata eseguita dalla riga di comando. Funzionava in modalità a 64 bit. I miei tempi sono leggermente diversi.

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

In particolare, PlusEqual(Element)è leggermente più veloce di PlusEqual(double, double).

Qualunque sia il problema in .NET 3.5, non sembra esistere in .NET 4.0.


2
Sì, la risposta su Structs sembra essere "ottieni il JIT più recente". Ma come ho chiesto nella risposta di Henk, perché i metodi sono molto più veloci degli operatori? Entrambi i tuoi metodi sono 5 volte più veloci dei tuoi operatori ... che stanno facendo esattamente la stessa cosa. È fantastico poter usare di nuovo gli struct ... ma è triste dover ancora evitare gli operatori.
Brian Kennedy,

Jim, sarei molto interessato a conoscere la versione del file C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll sul tuo sistema ... se più recente del mio (.3620), ma più vecchia rispetto a Corey (.5446), questo potrebbe spiegare perché i tuoi operatori sono ancora lenti come il mio, ma Corey non lo sono.
Brian Kennedy,

@Brian: versione del file 2.0.50727.4214.
Jim Mischel

GRAZIE! Quindi, devo assicurarmi che i miei utenti abbiano 4214 o successivo per ottenere le ottimizzazioni della struttura e 5446 o successivo per ottenere l'ottimizzazione dell'operatore. Devo aggiungere del codice per verificarlo all'avvio e fornire alcuni avvisi. Grazie ancora.
Brian Kennedy,

2

Come @Corey Kosak, ho appena eseguito questo codice in VS 2010 Express come una semplice app console in modalità di rilascio. Ottengo numeri molto diversi. Ma ho anche Fx4.5 quindi questi potrebbero non essere i risultati per un Fx4.0 pulito.

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

Modifica: e ora esegui dalla riga cmd. Ciò fa la differenza e meno variazione nei numeri.


Sì, sembra che il successivo JIT abbia risolto il problema della struttura, ma rimane la mia domanda sul perché i metodi siano così più veloci degli operatori. Guarda quanto sono più veloci entrambi i metodi PlusEqual dell'operatore + = equivalente. Ed è anche interessante quanto più veloce - = sia di + = ... i tuoi tempi sono i primi in cui l'ho visto.
Brian Kennedy,

Henk, sarei molto interessato a conoscere la versione del file C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll sul tuo sistema ... se più recente del mio (.3620), ma più vecchia rispetto a Corey (.5446), questo potrebbe spiegare perché i tuoi operatori sono ancora lenti come il mio, ma Corey non lo sono.
Brian Kennedy,

1
Posso trovare solo la versione .50727 ma non sono sicuro che sia rilevante per Fx40 / Fx45?
Henk Holterman

Devi andare in Proprietà e fare clic sulla scheda Versione per vedere il resto del numero di versione.
Brian Kennedy,

2

Oltre alle differenze del compilatore JIT menzionate in altre risposte, un'altra differenza tra una chiamata al metodo struct e un operatore struct è che una chiamata al metodo struct passerà thiscome refparametro (e può essere scritta per accettare anche altri parametri come refparametri), mentre un L'operatore struct passerà tutti gli operandi per valore. Il costo per passare una struttura di qualsiasi dimensione come refparametro è fisso, non importa quanto grande sia la struttura, mentre il costo per passare strutture più grandi è proporzionale alla dimensione della struttura. Non c'è niente di sbagliato nell'usare grandi strutture (anche centinaia di byte) se si può evitare di copiarle inutilmente ; mentre le copie non necessarie possono spesso essere prevenute quando si utilizzano metodi, non possono essere prevenute quando si utilizzano operatori.


Hmmm ... beh, questo potrebbe spiegare molto! Quindi, se l'operatore è abbastanza breve da essere inline, presumo che non farà copie non necessarie. Ma se no, e la tua struttura è più di una parola, potresti non volerla implementare come operatore se la velocità è critica. Grazie per questa intuizione.
Brian Kennedy

A proposito, una cosa che mi infastidisce leggermente quando alle domande sulla velocità viene risposto "benchmark it!" è che tale risposta ignora il fatto che in molti casi ciò che conta è se un'operazione richiede solitamente 10 o 20 us, ma se un leggero cambiamento di circostanze potrebbe far sì che richieda 1 ms o 10 ms. Ciò che conta non è la velocità con cui qualcosa gira sulla macchina di uno sviluppatore, ma piuttosto se l'operazione sarà mai abbastanza lenta da avere importanza ; se il metodo X funziona due volte più velocemente del metodo Y sulla maggior parte delle macchine, ma su alcune macchine sarà 100 volte più lento, il metodo Y potrebbe essere la scelta migliore.
supercat

Ovviamente, qui stiamo parlando solo di 2 doppie ... non di grandi strutture. Passare due doppi nello stack in cui è possibile accedervi rapidamente non è necessariamente più lento del passare "this" nello stack e quindi dover dereferenziarlo per estrarli per operare su di essi .. ma potrebbe causare differenze. Tuttavia, in questo caso, dovrebbe essere inline, quindi l'ottimizzatore JIT dovrebbe finire con esattamente lo stesso codice.
Brian Kennedy

1

Non sono sicuro che sia rilevante, ma ecco i numeri per .NET 4.0 a 64 bit su Windows 7 a 64 bit. La mia versione di mscorwks.dll è 2.0.50727.5446. Ho appena incollato il codice in LINQPad e l'ho eseguito da lì. Ecco il risultato:

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

2
Interessante ... sembrerebbe che le ottimizzazioni aggiunte all'ottimizzatore JIT 32b non siano ancora arrivate all'ottimizzatore JIT 64b ... i tuoi rapporti sono ancora molto simili ai miei. Deludente ... ma buono a sapersi.
Brian Kennedy

0

Immagino che quando si accede ai membri della struttura, si stia facendo un'operazione extra per accedere al membro, il puntatore QUESTO + offset.


1
Bene, con un oggetto classe, avresti assolutamente ragione ... perché al metodo verrebbe semplicemente passato il puntatore 'this'. Tuttavia, con gli struct, non dovrebbe essere così. La struttura dovrebbe essere passata ai metodi sullo stack. Quindi, il primo double dovrebbe trovarsi dove sarebbe il puntatore "this" e il secondo double nella posizione subito dopo ... entrambi potrebbero essere registrati nella CPU. Quindi, il JIT dovrebbe usare al massimo un offset.
Brian Kennedy,

0

Può essere al posto di List dovresti usare double [] con offset "noti" e incrementi di indice?

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.