Pourquoi les opérateurs sont-ils tellement plus lents que les appels de méthode? (les structures sont plus lentes uniquement sur les anciens JIT)


84

Intro: J'écris du code haute performance en C #. Oui, je sais que C ++ me donnerait une meilleure optimisation, mais je choisis toujours d'utiliser C #. Je ne souhaite pas débattre de ce choix. J'aimerais plutôt entendre ceux qui, comme moi, essaient d'écrire du code haute performance sur le .NET Framework.

Des questions:

  • Pourquoi l'opérateur dans le code ci-dessous est-il plus lent que l'appel de méthode équivalent ??
  • Pourquoi la méthode passe-t-elle deux doubles dans le code ci-dessous plus rapidement que la méthode équivalente passant une structure qui a deux doubles à l'intérieur? (A: les anciens JIT optimisent mal les structures)
  • Existe-t-il un moyen pour que le compilateur .NET JIT traite des structures simples aussi efficacement que les membres de la structure? (A: obtenir un JIT plus récent)

Ce que je pense savoir: le compilateur .NET JIT original n'inclurait rien qui impliquait une structure. Les structures données bizarres ne doivent être utilisées que lorsque vous avez besoin de petits types de valeur qui doivent être optimisés comme des éléments intégrés, mais true. Heureusement, dans .NET 3.5SP1 et .NET 2.0SP2, ils ont apporté quelques améliorations à l'optimiseur JIT, y compris des améliorations à l'inlining, en particulier pour les structures. (Je suppose qu'ils l'ont fait parce que sinon, la nouvelle structure Complex qu'ils introduisaient aurait fonctionné horriblement ... donc l'équipe Complex était probablement en train de marteler l'équipe JIT Optimizer.) Donc, toute documentation antérieure à .NET 3.5 SP1 est probablement pas trop pertinent pour ce problème.

Ce que mes tests montrent: j'ai vérifié que je disposais du plus récent JIT Optimizer en vérifiant que le fichier C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll a une version> = 3053 et devrait donc avoir ces améliorations à l'optimiseur JIT. Cependant, même avec cela, ce que mes horaires et mes regards sur le démontage montrent tous les deux sont:

Le code produit par JIT pour passer une structure avec deux doubles est beaucoup moins efficace que le code qui passe directement les deux doubles.

Le code produit par JIT pour une méthode struct passe 'this' beaucoup plus efficacement que si vous passiez une structure en argument.

Le JIT est toujours mieux aligné si vous passez deux doubles plutôt que de passer une structure avec deux doubles, même avec le multiplicateur car il est clairement dans une boucle.

Les timings: En fait, en regardant le démontage, je me rends compte que la plupart du temps, dans les boucles, il suffit d'accéder aux données de test hors de la liste. La différence entre les quatre façons d'effectuer les mêmes appels est radicalement différente si vous prenez en compte le code de surcharge de la boucle et l'accès aux données. J'obtiens des accélérations de 5x à 20x pour faire PlusEqual (double, double) au lieu de PlusEqual (Element). Et 10x à 40x pour faire PlusEqual (double, double) au lieu de operator + =. Sensationnel. Triste.

Voici un ensemble de minutages:

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

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

L'IL: (c'est-à-dire dans quoi certains des éléments ci-dessus sont compilés)

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, cela devrait être référencé comme un exemple de ce à quoi une bonne question sur Stackoverflow peut ressembler! Seuls les commentaires générés automatiquement peuvent être omis. Malheureusement, j'en sais trop peu pour me plonger dans le problème, mais j'aime vraiment la question!
Dennis Traub

2
Je ne pense pas qu'un test unitaire soit un bon endroit pour exécuter un benchmark.
Henk Holterman le

1
Pourquoi la structure doit-elle être plus rapide que deux doubles? Dans .NET, la structure n'est JAMAIS égale à la somme des tailles de ses membres. Donc, par définition, il est plus grand, donc par définition, il doit être plus lent à pousser sur la pile, puis juste 2 valeurs doubles. Si le compilateur insère le paramètre struct dans la mémoire double de la ligne 2, que se passe-t-il si à l'intérieur de la méthode vous voulez accéder à cette structure avec réflexion. Où seront les informations d'exécution liées à cet objet struct? N'est-ce pas, ou il me manque quelque chose?
Tigran

3
@Tigran: Vous avez besoin de sources pour ces affirmations. Je crois que tu as tort. Ce n'est que lorsqu'un type de valeur est encadré que les métadonnées doivent être stockées avec la valeur. Dans une variable de type struct statique, il n'y a pas de surcharge.
Ben Voigt

1
Je pensais que la seule chose qui manquait était l'assemblage. Et maintenant vous l'avez ajouté (veuillez noter que c'est l'assembleur x86 et PAS MSIL).
Ben Voigt

Réponses:


9

J'obtiens des résultats très différents, beaucoup moins dramatiques. Mais je n'ai pas utilisé le testeur, j'ai collé le code dans une application en mode console. Le résultat de 5% est ~ 87% en mode 32 bits, ~ 100% en mode 64 bits lorsque je l'essaie.

L'alignement est essentiel sur les doubles, le runtime .NET ne peut promettre qu'un alignement de 4 sur une machine 32 bits. Il me semble que le lanceur de tests démarre les méthodes de test avec une adresse de pile alignée sur 4 au lieu de 8. La pénalité de désalignement devient très importante lorsque le double franchit une limite de ligne de cache.


Pourquoi .NET peut fondamentalement réussir sur l'alignement de seulement 4 doubles? L'alignement est effectué en utilisant des blocs de 4 octets sur une machine 32 bits. Quel est le problème là-bas?
Tigran

Pourquoi le runtime ne s'aligne-t-il que sur 4 octets sur x86? Je pense qu'il pourrait s'aligner sur 64 bits s'il prend des précautions supplémentaires lorsque du code non géré appelle du code géré. Bien que la spécification n'offre que de faibles garanties d'alignement, les implémentations devraient pouvoir s'aligner plus strictement. (Spec: "Les données de 8 octets sont correctement alignées lorsqu'elles sont stockées sur la même limite requise par le matériel sous-jacent pour l'accès atomique à un
entier

1
@Code - Eh bien, c'est possible, les générateurs de code C le font en faisant des maths sur le pointeur de pile dans le prologue de la fonction. La gigue x86 ne l'est tout simplement pas. C'est beaucoup plus important pour les langages natifs car l'allocation de tableaux sur la pile est beaucoup plus courante et ils ont un allocateur de tas qui s'aligne sur 8 et ne voudraient donc jamais rendre les allocations de pile moins efficaces que les allocations de tas. Nous sommes coincés avec un alignement de 4 à partir du tas 32 bits gc.
Hans Passant

5

J'ai du mal à reproduire vos résultats.

J'ai pris votre code:

  • en a fait une application console autonome
  • construit une version optimisée (version)
  • augmentation du facteur «taille» de 2,5 millions à 10 millions
  • l'a exécuté à partir de la ligne de commande (en dehors de l'IDE)

Quand je l'ai fait, j'ai eu les horaires suivants qui sont très différents du vôtre. Pour éviter tout doute, je posterai exactement le code que j'ai utilisé.

Voici mes horaires

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

Et voici mes modifications de votre 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);
    }
  }
}

J'ai juste fait la même chose, mes résultats ressemblent plus aux vôtres. Veuillez indiquer la plate-forme et le type de CPu.
Henk Holterman

Très intéressant! J'ai demandé à d'autres de vérifier mes résultats ... vous êtes le premier à être différent. Première question pour vous: quel est le numéro de version du fichier que je mentionne dans mon message ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... c'est celui indiqué par les documents Microsoft la version de JIT Optimizer dont vous disposez. (Si je peux simplement dire à mes utilisateurs de mettre à jour leur .NET pour voir de grandes accélérations, je serai un campeur heureux. Mais je suppose que ce ne sera pas aussi simple.)
Brian Kennedy

Je courais dans Visual Studio ... fonctionnant sur Windows XP SP3 ... dans une machine virtuelle VMware ... sur un Intel Core i7 à 2,7 GHz. Mais ce ne sont pas les temps absolus qui m'intéressent ... ce sont les ratios ... Je m'attendrais à ce que ces trois méthodes fonctionnent toutes de la même manière, ce qu'elles ont fait pour Corey, mais PAS pour moi.
Brian Kennedy

Les propriétés de mon projet disent: Configuration: Release; Plate-forme: active (x86); Plate-forme cible: x86
Corey Kosak

1
Concernant votre demande d'obtenir la version de mscorwks ... Désolé, vouliez-vous que je lance cette chose contre .NET 2.0? Mes tests étaient sur .NET 4.0
Corey Kosak

3

Exécuter .NET 4.0 ici. J'ai compilé avec "Any CPU", ciblant .NET 4.0 en mode release. L'exécution était à partir de la ligne de commande. Il a fonctionné en mode 64 bits. Mes horaires sont un peu différents.

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

En particulier, PlusEqual(Element)est légèrement plus rapide que PlusEqual(double, double).

Quel que soit le problème dans .NET 3.5, il ne semble pas exister dans .NET 4.0.


2
Oui, la réponse sur Structs semble être "obtenir le nouveau JIT". Mais comme je l'ai demandé sur la réponse de Henk, pourquoi les méthodes sont-elles tellement plus rapides que les opérateurs? Vos deux méthodes sont 5 fois plus rapides que l'un ou l'autre de vos opérateurs ... qui font exactement la même chose. C'est génial que je puisse à nouveau utiliser des structs ... mais triste de devoir encore éviter les opérateurs.
Brian Kennedy

Jim, je serais très intéressé de connaître la version du fichier C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll sur votre système ... si plus récent que le mien (.3620), mais plus ancien que Corey (.5446), cela pourrait expliquer pourquoi vos opérateurs sont toujours lents comme le mien, mais Corey ne le sont pas.
Brian Kennedy

@Brian: version du fichier 2.0.50727.4214.
Jim Mischel

MERCI! Donc, je dois m'assurer que mes utilisateurs ont 4214 ou version ultérieure pour obtenir les optimisations de structure et 5446 ou version ultérieure pour obtenir l'optimisation de l'opérateur. J'ai besoin d'ajouter du code pour vérifier cela au démarrage et donner quelques avertissements. Merci encore.
Brian Kennedy

2

Comme @Corey Kosak, je viens d'exécuter ce code dans VS 2010 Express comme une simple application console en mode Release. J'obtiens des chiffres très différents. Mais j'ai aussi Fx4.5 donc ce ne sont peut-être pas les résultats pour un Fx4.0 propre.

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

Edit: et maintenant exécutez à partir de la ligne cmd. Cela fait une différence et moins de variation dans les chiffres.


Oui, il semble que le dernier JIT ait résolu le problème de structure, mais ma question sur la raison pour laquelle les méthodes sont tellement plus rapides que les opérateurs demeure. Regardez à quel point les deux méthodes PlusEqual sont beaucoup plus rapides que l'opérateur + = équivalent. Et c'est aussi intéressant à quel point - = est beaucoup plus rapide que + = ... vos horaires sont les premiers où j'ai vu cela.
Brian Kennedy le

Henk, je serais très intéressé de connaître la version du fichier C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll sur votre système ... si plus récent que le mien (.3620), mais plus ancien que Corey (.5446), cela pourrait expliquer pourquoi vos opérateurs sont toujours lents comme le mien, mais Corey ne le sont pas.
Brian Kennedy

1
Je ne peux trouver que la version .50727 mais je ne suis pas sûr que cela soit pertinent pour Fx40 / Fx45?
Henk Holterman

Vous devez aller dans Propriétés et cliquer sur l'onglet Version pour voir le reste du numéro de version.
Brian Kennedy

2

En plus des différences du compilateur JIT mentionnées dans d'autres réponses, une autre différence entre un appel de méthode struct et un opérateur struct est qu'un appel de méthode struct passera en thistant que refparamètre (et peut être écrit pour accepter d'autres paramètres comme refparamètres également), tandis qu'un L'opérateur struct passera tous les opérandes par valeur. Le coût de passage d'une structure de toute taille en tant que refparamètre est fixe, quelle que soit la taille de la structure, tandis que le coût de passage de structures plus grandes est proportionnel à la taille de la structure. Il n'y a rien de mal à utiliser de grandes structures (même des centaines d'octets) si l'on peut éviter de les copier inutilement ; si les copies inutiles peuvent souvent être évitées lors de l'utilisation de méthodes, elles ne peuvent pas être évitées lors de l'utilisation d'opérateurs.


Hmmm ... eh bien, cela pourrait expliquer beaucoup de choses! Donc, si l'opérateur est suffisamment court pour qu'il soit intégré, je suppose qu'il ne fera pas de copies inutiles. Mais sinon, et votre structure comprend plus d'un mot, vous ne voudrez peut-être pas l'implémenter en tant qu'opérateur si la vitesse est critique. Merci pour cette idée.
Brian Kennedy

BTW, une chose qui m'ennuie légèrement lorsque des questions sur la vitesse sont répondues "benchmark it!" est qu'une telle réponse ne tient pas compte du fait que dans de nombreux cas, ce qui compte est de savoir si une opération prend habituellement 10us ou 20us, mais si un léger changement de circonstances pourrait la faire prendre 1 ms ou 10 ms. Ce qui compte, ce n'est pas à quelle vitesse quelque chose s'exécute sur la machine d'un développeur, mais plutôt si l'opération sera jamais assez lente pour avoir de l'importance ; si la méthode X fonctionne deux fois plus vite que la méthode Y sur la plupart des machines, mais que sur certaines machines, elle sera 100 fois plus lente, la méthode Y peut être le meilleur choix.
supercat

Bien sûr, nous parlons ici de seulement 2 doubles ... pas de grandes structures. Passer deux doubles sur la pile où ils peuvent être rapidement accédés n'est pas nécessairement plus lent que de passer «ceci» sur la pile et de devoir ensuite le déréférencer pour les attirer pour les opérer… mais cela pourrait causer des différences. Cependant, dans ce cas, il doit être inséré, de sorte que JIT Optimizer doit se retrouver avec exactement le même code.
Brian Kennedy

1

Je ne sais pas si cela est pertinent, mais voici les chiffres pour .NET 4.0 64 bits sur Windows 7 64 bits. Ma version mscorwks.dll est 2.0.50727.5446. Je viens de coller le code dans LINQPad et je l'ai exécuté à partir de là. Voici le résultat:

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
Intéressant ... il semblerait que les optimisations qui ont été ajoutées à l'Optimiseur JIT 32b ne soient pas encore arrivées à l'Optimiseur JIT 64b ... vos ratios sont toujours très similaires aux miens. Décevant ... mais bon à savoir.
Brian Kennedy

0

J'imagine que lorsque vous accédez aux membres de la structure, c'est en fait une opération supplémentaire pour accéder au membre, le pointeur THIS + offset.


1
Eh bien, avec un objet de classe, vous auriez absolument raison ... parce que la méthode ne ferait que passer le pointeur «this». Cependant, avec les structures, cela ne devrait pas être le cas. La structure doit être passée dans les méthodes de la pile. Ainsi, le premier double devrait être assis là où le pointeur «this» serait et le deuxième double dans la position juste après lui ... tous deux étant probablement des registres dans le CPU. Ainsi, le JIT devrait simplement utiliser un décalage au maximum.
Brian Kennedy

0

Peut-être qu'au lieu de List, vous devriez utiliser double [] avec des décalages et des incréments d'index "connus"?

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.