c# - ternario - ¿Por qué los operadores son mucho más lentos que las llamadas a métodos?(las estructuras son más lentas solo en JIT antiguos)
prioridad de operadores en programacion (8)
¿Puede ser en lugar de List que debe usar double [] con compensaciones "conocidas" e incrementos de índice?
Introducción: escribo código de alto rendimiento en C #. Sí, sé que C ++ me daría una mejor optimización, pero todavía elijo usar C #. No deseo debatir esa elección. Más bien, me gustaría saber de aquellos que, como yo, intentan escribir código de alto rendimiento en .NET Framework.
Preguntas:
- ¿Por qué el operador en el código a continuación es más lento que el método equivalente?
- ¿Por qué el método pasa dos dobles en el código de abajo más rápido que el método equivalente que pasa una estructura que tiene dos dobles adentro? (A: los JIT anteriores optimizan las estructuras mal)
- ¿Hay alguna manera de hacer que el compilador .NET JIT trate las estructuras simples tan eficientemente como los miembros de la estructura? (A: obtener un JIT más nuevo)
Lo que creo que sé: el compilador .NET JIT original no alinearía nada que implique una estructura. Las estructuras dadas de Bizarre solo deben usarse donde necesite tipos de valores pequeños que deberían optimizarse como incorporados, pero verdaderos. Afortunadamente, en .NET 3.5SP1 y .NET 2.0SP2, hicieron algunas mejoras en JIT Optimizer, incluidas mejoras en la creación de líneas, particularmente para las estructuras. (Supongo que lo hicieron porque, de lo contrario, la nueva estructura Complex que estaban presentando habría tenido un desempeño horrible ... por lo que el equipo Complex probablemente estaba golpeando al equipo de JIT Optimizer.) Por lo tanto, cualquier documentación anterior a .NET 3.5 SP1 es probablemente no muy relevante para este tema
Lo que muestra mi prueba: he verificado que tengo el JIT Optimizer más nuevo comprobando que el archivo C: / Windows / Microsoft.NET / Framework / v2.0.50727 / mscorwks.dll tiene la versión> = 3053, por lo que debería tener esas mejoras al optimizador JIT. Sin embargo, incluso con eso, lo que muestran mis tiempos y miras al desmontaje son:
El código producido por JIT para pasar una estructura con dos dobles es mucho menos eficiente que el código que pasa directamente los dos dobles.
El código producido por JIT para un método struct pasa en ''esto'' mucho más eficientemente que si pasara una estructura como argumento.
El JIT todavía se alinea mejor si pasas dos dobles en lugar de pasar una estructura con dos dobles, incluso con el multiplicador debido a que está claramente en un bucle.
Los tiempos: en realidad, mirando el desmontaje me doy cuenta de que la mayor parte del tiempo en los bucles es simplemente acceder a los datos de prueba fuera de la lista. La diferencia entre las cuatro formas de hacer las mismas llamadas es radicalmente diferente si se factoriza el código de sobrecarga del ciclo y el acceso a los datos. Tengo entre 5 y 20 aumentos de velocidad para hacer PlusEqual (doble, doble) en lugar de PlusEqual (Elemento). Y de 10x a 40x para hacer PlusEqual (doble, doble) en lugar de operador + =. Guau. Triste.
Aquí hay un conjunto de sincronizaciones:
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%.
El código:
namespace OperatorVsMethod
{
public struct Element
{
public double Left;
public double Right;
public Element(double left, double right)
{
this.Left = left;
this.Right = right;
}
public static Element operator +(Element x, Element y)
{
return new Element(x.Left + y.Left, x.Right + y.Right);
}
public static Element operator -(Element x, Element y)
{
x.Left += y.Left;
x.Right += y.Right;
return x;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(Element that)
{
this.Left += that.Left;
this.Right += that.Right;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(double thatLeft, double thatRight)
{
this.Left += thatLeft;
this.Right += thatRight;
}
}
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Stopwatch stopwatch = new Stopwatch();
// Populate a List of Elements to multiply together
int seedSize = 4;
List<double> doubles = new List<double>(seedSize);
doubles.Add(2.5d);
doubles.Add(100000d);
doubles.Add(-0.5d);
doubles.Add(-100002d);
int size = 2500000 * seedSize;
List<Element> elts = new List<Element>(size);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
int di = ii % seedSize;
double d = doubles[di];
elts.Add(new Element(d, d));
}
stopwatch.Stop();
long populateMS = stopwatch.ElapsedMilliseconds;
// Measure speed of += operator (calls ctor)
Element operatorCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorCtorResult += elts[ii];
}
stopwatch.Stop();
long operatorCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of -= operator (+= without ctor)
Element operatorNoCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorNoCtorResult -= elts[ii];
}
stopwatch.Stop();
long operatorNoCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(Element) method
Element plusEqualResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
plusEqualResult.PlusEqual(elts[ii]);
}
stopwatch.Stop();
long plusEqualMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(double, double) method
Element plusEqualDDResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
plusEqualDDResult.PlusEqual(elt.Left, elt.Right);
}
stopwatch.Stop();
long plusEqualDDMS = stopwatch.ElapsedMilliseconds;
// Measure speed of doing nothing but accessing the Element
Element doNothingResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
double left = elt.Left;
double right = elt.Right;
}
stopwatch.Stop();
long doNothingMS = stopwatch.ElapsedMilliseconds;
// Report results
Assert.AreEqual(1d, operatorCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, operatorNoCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualDDResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, doNothingResult.Left, "The operator += did not compute the right result!");
// Report speeds
Console.WriteLine("Populating List<Element> took {0}ms.", populateMS);
Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS);
Console.WriteLine("The ''same'' += operator took {0}ms.", operatorCtorMS);
Console.WriteLine("The ''same'' -= operator took {0}ms.", operatorNoCtorMS);
Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS);
Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS);
// Compare speeds
long percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
operatorCtorMS -= doNothingMS;
operatorNoCtorMS -= doNothingMS;
plusEqualMS -= doNothingMS;
plusEqualDDMS -= doNothingMS;
Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List...");
percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
}
}
}
IL: (también conocido como lo que algunos de los anteriores se compilan)
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
Además de las diferencias del compilador JIT mencionadas en otras respuestas, otra diferencia entre una llamada al método struct y un operador struct es que una llamada al método struct pasará this
como un parámetro ref
(y puede escribirse para aceptar otros parámetros como parámetros ref
) , mientras que un operador struct pasará todos los operandos por valor. El costo de pasar una estructura de cualquier tamaño como un parámetro ref
es fijo, sin importar cuán grande sea la estructura, mientras que el costo para pasar estructuras más grandes es proporcional al tamaño de la estructura. No hay nada de malo en usar estructuras grandes (incluso cientos de bytes) si se puede evitar copiarlas innecesariamente ; mientras que las copias innecesarias a menudo se pueden evitar al usar métodos, no se pueden evitar al usar operadores.
Al igual que @Corey Kosak, acabo de ejecutar este código en VS 2010 Express como una simple aplicación de consola en el modo de lanzamiento. Obtengo números muy diferentes. Pero también tengo Fx4.5 así que estos pueden no ser los resultados para un Fx4.0 limpio.
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%.
Editar: y ahora se ejecuta desde la línea de cmd. Eso hace una diferencia, y menos variación en los números.
Ejecutando .NET 4.0 aquí. Compilé con "Cualquier CPU", apuntando a .NET 4.0 en modo de lanzamiento. La ejecución fue desde la línea de comando. Se ejecutó en modo de 64 bits. Mis tiempos son un poco diferentes.
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 particular, PlusEqual(Element)
es ligeramente más rápido que PlusEqual(double, double)
.
Cualquiera que sea el problema en .NET 3.5, no parece existir en .NET 4.0.
Estoy obteniendo resultados muy diferentes, mucho menos dramáticos. Pero no usé el corrector de prueba, pegué el código en una aplicación de modo de consola. El resultado del 5% es ~ 87% en modo de 32 bits, ~ 100% en modo de 64 bits cuando lo intento.
La alineación es crítica en dobles, el tiempo de ejecución .NET solo puede prometer una alineación de 4 en una máquina de 32 bits. Me parece que el corredor de prueba está comenzando los métodos de prueba con una dirección de pila que está alineada con 4 en lugar de 8. La penalización de desalineación es muy grande cuando el doble cruza un límite de línea de caché.
Me imagino que cuando estás accediendo a los miembros de la estructura, de hecho está haciendo una operación extra para acceder al miembro, el puntero THIS + offset.
No estoy seguro de si esto es relevante, pero aquí están los números para .NET 4.0 de 64 bits en Windows 7 de 64 bits. Mi versión mscorwks.dll es 2.0.50727.5446. Acabo de pegar el código en LINQPad y lo ejecuté desde allí. Este es el resultado:
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%.
Tengo dificultades para replicar tus resultados.
Tomé tu código:
- lo convirtió en una aplicación de consola independiente
- construyó una versión optimizada
- aumentó el factor de "tamaño" de 2.5M a 10M
- lo ejecutó desde la línea de comando (fuera del IDE)
Cuando lo hice, obtuve los siguientes tiempos, que son muy diferentes a los tuyos. Para evitar dudas, publicaré exactamente el código que utilicé.
Aquí están mis tiempos
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%.
Y estas son mis ediciones a tu código:
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);
}
}
}