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
la source
Réponses:
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.
la source
J'ai du mal à reproduire vos résultats.
J'ai pris votre code:
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); } } }
la source
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 quePlusEqual(double, double)
.Quel que soit le problème dans .NET 3.5, il ne semble pas exister dans .NET 4.0.
la source
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.
la source
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
this
tant queref
paramètre (et peut être écrit pour accepter d'autres paramètres commeref
paramè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 queref
paramè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.la source
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%.
la source
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.
la source
Peut-être qu'au lieu de List, vous devriez utiliser double [] avec des décalages et des incréments d'index "connus"?
la source