¿Por qué los operadores son mucho más lentos que las llamadas a métodos? (las estructuras son más lentas solo en JIT más antiguos)

84

Introducción: escribo código de alto rendimiento en C #. Sí, sé que C ++ me brindaría una mejor optimización, pero aún así elijo usar C #. No deseo debatir esa elección. Más bien, me gustaría escuchar a aquellos que, como yo, están tratando de escribir código de alto rendimiento en .NET Framework.

Preguntas:

  • ¿Por qué el operador en el código siguiente es más lento que la llamada al método equivalente?
  • ¿Por qué el método que pasa dos dobles en el código siguiente es más rápido que el método equivalente que pasa una estructura que tiene dos dobles adentro? (A: los JIT más antiguos optimizan las estructuras de manera deficiente)
  • ¿Hay alguna manera de hacer que el compilador .NET JIT trate las estructuras simples con la misma eficacia que los miembros de la estructura? (A: obtenga un JIT más nuevo)

Lo que creo que sé: el compilador JIT de .NET original no incluía nada que involucrara una estructura. Las estructuras dadas extrañas solo deben usarse cuando necesite tipos de valores pequeños que deben optimizarse como incorporados, pero verdaderos. Afortunadamente, en .NET 3.5SP1 y .NET 2.0SP2, realizaron algunas mejoras en JIT Optimizer, incluidas mejoras en la alineación, especialmente para estructuras. (Supongo que lo hicieron porque, de lo contrario, la nueva estructura Complex que estaban introduciendo habría funcionado horriblemente ... por lo que el equipo Complex probablemente estaba golpeando al equipo JIT Optimizer). Entonces, cualquier documentación anterior a .NET 3.5 SP1 probablemente sea no es demasiado relevante para este tema.

Lo que muestran mis pruebas: he verificado que tengo el optimizador JIT más nuevo comprobando que el archivo C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll tiene la versión> = 3053 y, por lo tanto, debería tener esas mejoras al optimizador JIT. Sin embargo, incluso con eso, lo que muestran mis tiempos y miradas en el 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 de estructura pasa 'esto' mucho más eficientemente que si pasara una estructura como argumento.

El JIT aún se alinea mejor si pasa 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 realizar las mismas llamadas es drásticamente diferente si se tiene en cuenta el código general del bucle y el acceso a los datos. Obtengo entre 5 y 20 aumentos de velocidad por hacer PlusEqual (doble, doble) en lugar de PlusEqual (Element). Y de 10x a 40x para hacer PlusEqual (doble, doble) en lugar de operador + =. Guau. Triste.

Aquí hay un conjunto de tiempos:

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

El IL: (también conocido como en qué se compilan algunos de los anteriores)

public void PlusEqual(Element that)
    {
00000000 push    ebp 
00000001 mov     ebp,esp 
00000003 push    edi 
00000004 push    esi 
00000005 push    ebx 
00000006 sub     esp,30h 
00000009 xor     eax,eax 
0000000b mov     dword ptr [ebp-10h],eax 
0000000e xor     eax,eax 
00000010 mov     dword ptr [ebp-1Ch],eax 
00000013 mov     dword ptr [ebp-3Ch],ecx 
00000016 cmp     dword ptr ds:[04C87B7Ch],0 
0000001d je     00000024 
0000001f call    753081B1 
00000024 nop       
      this.Left += that.Left;
00000025 mov     eax,dword ptr [ebp-3Ch] 
00000028 fld     qword ptr [ebp+8] 
0000002b fadd    qword ptr [eax] 
0000002d fstp    qword ptr [eax] 
      this.Right += that.Right;
0000002f mov     eax,dword ptr [ebp-3Ch] 
00000032 fld     qword ptr [ebp+10h] 
00000035 fadd    qword ptr [eax+8] 
00000038 fstp    qword ptr [eax+8] 
    }
0000003b nop       
0000003c lea     esp,[ebp-0Ch] 
0000003f pop     ebx 
00000040 pop     esi 
00000041 pop     edi 
00000042 pop     ebp 
00000043 ret     10h 
 public void PlusEqual(double thatLeft, double thatRight)
    {
00000000 push    ebp 
00000001 mov     ebp,esp 
00000003 push    edi 
00000004 push    esi 
00000005 push    ebx 
00000006 sub     esp,30h 
00000009 xor     eax,eax 
0000000b mov     dword ptr [ebp-10h],eax 
0000000e xor     eax,eax 
00000010 mov     dword ptr [ebp-1Ch],eax 
00000013 mov     dword ptr [ebp-3Ch],ecx 
00000016 cmp     dword ptr ds:[04C87B7Ch],0 
0000001d je     00000024 
0000001f call    75308159 
00000024 nop       
      this.Left += thatLeft;
00000025 mov     eax,dword ptr [ebp-3Ch] 
00000028 fld     qword ptr [ebp+10h] 
0000002b fadd    qword ptr [eax] 
0000002d fstp    qword ptr [eax] 
      this.Right += thatRight;
0000002f mov     eax,dword ptr [ebp-3Ch] 
00000032 fld     qword ptr [ebp+8] 
00000035 fadd    qword ptr [eax+8] 
00000038 fstp    qword ptr [eax+8] 
    }
0000003b nop       
0000003c lea     esp,[ebp-0Ch] 
0000003f pop     ebx 
00000040 pop     esi 
00000041 pop     edi 
00000042 pop     ebp 
00000043 ret     10h 
Brian Kennedy
fuente
22
¡Vaya, esto debería ser referenciado como un ejemplo de cómo puede verse una buena pregunta en Stackoverflow! Solo se pueden omitir los comentarios generados automáticamente. Desafortunadamente, sé muy poco para sumergirme en el problema, ¡pero realmente me gusta la pregunta!
Dennis Traub
2
No creo que una prueba unitaria sea un buen lugar para ejecutar un punto de referencia.
Henk Holterman
1
¿Por qué la estructura tiene que ser más rápida que dos dobles? En .NET, la estructura NUNCA es igual a la suma de tamaños de sus miembros. Entonces, por definición, es más grande, por lo que, por definición, tiene que ser más lento empujando en la pila, luego solo 2 valores dobles. Si el compilador integra el parámetro de estructura en la fila 2 con doble memoria, ¿qué pasa si dentro del método desea acceder a esa estructura con reflexión? ¿Dónde estará la información en tiempo de ejecución vinculada a ese objeto de estructura? ¿No es así o me falta algo?
Tigran
3
@Tigran: Necesitas fuentes para esas afirmaciones. Creo que estas equivocado. Solo cuando un tipo de valor se encuadra, los metadatos deben almacenarse con el valor. En una variable con tipo de estructura estática, no hay sobrecarga.
Ben Voigt
1
Pensaba que lo único que faltaba era el montaje. Y ahora ha agregado eso (tenga en cuenta que es ensamblador x86 y NO MSIL).
Ben Voigt

Respuestas:

9

Obtengo resultados muy diferentes, mucho menos dramáticos. Pero no usé el ejecutor de prueba, pegué el código en una aplicación en modo 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 fundamental en los dobles, el tiempo de ejecución de .NET solo puede prometer una alineación de 4 en una máquina de 32 bits. Me parece que el corredor de pruebas está iniciando los métodos de prueba con una dirección de pila alineada con 4 en lugar de 8. La penalización por desalineación se vuelve muy grande cuando el doble cruza el límite de una línea de caché.

Hans Passant
fuente
¿Por qué .NET básicamente puede tener éxito en la alineación de solo 4 dobles? La alineación se realiza utilizando fragmentos de 4 bytes en una máquina de 32 bits. ¿Qué problema hay ahí?
Tigran
¿Por qué el tiempo de ejecución solo se alinea a 4 bytes en x86? Creo que podría alinearse a 64 bits si tiene más cuidado cuando el código no administrado llama al código administrado. Si bien la especificación solo tiene garantías de alineación débiles, las implementaciones deberían poder alinearse de manera más estricta. (Especificación: "Los datos de 8 bytes se alinean correctamente cuando se almacenan en el mismo límite requerido por el hardware subyacente para el acceso atómico a un int nativo")
CodesInChaos
1
@Code - Bueno, podría, los generadores de código C hacen esto haciendo matemáticas en el puntero de la pila en el prólogo de la función. El jitter x86 simplemente no lo hace. Es mucho más importante para los idiomas nativos, ya que la asignación de matrices en la pila es mucho más común y tienen un asignador de montón que se alinea con 8, por lo que nunca querría que las asignaciones de pila sean menos eficientes que las asignaciones de montón. Estamos atascados con una alineación de 4 del montón gc de 32 bits.
Hans Passant
5

Tengo algunas dificultades para replicar tus resultados.

Tomé tu código:

  • la convirtió en una aplicación de consola independiente
  • construyó una compilación optimizada (de lanzamiento)
  • aumentó el factor de "tamaño" de 2,5 M a 10 M
  • lo ejecutó desde la línea de comando (fuera del IDE)

Cuando lo hice, obtuve los siguientes tiempos que son muy diferentes a los suyos. 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 de 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);
    }
  }
}
Corey Kosak
fuente
Simplemente hice lo mismo, mis resultados son más parecidos a los tuyos. Indique la plataforma y el tipo de CPu.
Henk Holterman
¡Muy interesante! Otros verificaron mis resultados ... eres el primero en ser diferente. Primera pregunta para usted: ¿cuál es el número de versión del archivo que menciono en mi publicación ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... ese es el que indicaron los documentos de Microsoft la versión de JIT Optimizer que tienes. (Si pudiera decirles a mis usuarios que actualicen su .NET para ver grandes aceleraciones, seré un campista feliz. Pero supongo que no será tan simple).
Brian Kennedy
Estaba ejecutando dentro de Visual Studio ... ejecutándome en Windows XP SP3 ... en una máquina virtual VMware ... en un Intel Core i7 de 2.7GHz. Pero no son los tiempos absolutos lo que me interesa ... son las proporciones ... Esperaría que esos tres métodos funcionen de manera similar, lo que hicieron para Corey, pero NO para mí.
Brian Kennedy
Las propiedades de mi proyecto dicen: Configuration: Release; Plataforma: Activa (x86); Objetivo de la plataforma: x86
Corey Kosak
1
Con respecto a su solicitud para obtener la versión de mscorwks ... Lo siento, ¿quería que ejecutara esto contra .NET 2.0? Mis pruebas fueron en .NET 4.0
Corey Kosak
3

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 comandos. Funcionó 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 un poco más rápido que PlusEqual(double, double).

Cualquiera que sea el problema en .NET 3.5, no parece existir en .NET 4.0.

Jim Mischel
fuente
2
Sí, la respuesta en Structs parece ser "obtenga el JIT más nuevo". Pero como pregunté sobre la respuesta de Henk, ¿por qué los métodos son mucho más rápidos que los operadores? Ambos métodos son 5 veces más rápidos que cualquiera de sus operadores ... que están haciendo exactamente lo mismo. Es genial que pueda usar estructuras nuevamente ... pero es triste que todavía tenga que evitar los operadores.
Brian Kennedy
Jim, me interesaría mucho saber la versión del archivo C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll en su sistema ... si es más nuevo que el mío (.3620), pero más antiguo que los de Corey (.5446), entonces eso podría explicar por qué sus operadores siguen siendo lentos como los míos, pero los de Corey no lo son.
Brian Kennedy
@Brian: Versión de archivo 2.0.50727.4214.
Jim Mischel
¡GRACIAS! Por lo tanto, necesito asegurarme de que mis usuarios tengan 4214 o posterior para obtener optimizaciones de estructura y 5446 o posterior para obtener optimización de operador. Necesito agregar un código para verificarlo al inicio y dar algunas advertencias. Gracias de nuevo.
Brian Kennedy
2

Al igual que @Corey Kosak, acabo de ejecutar este código en VS 2010 Express como una aplicación de consola simple en el modo de lanzamiento. Obtengo números muy diferentes. Pero también tengo Fx4.5, por lo 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 ejecuta desde la línea cmd. Eso hace una diferencia y menos variación en los números.

Henk Holterman
fuente
Sí, parece que el JIT posterior ha solucionado el problema de la estructura, pero permanece mi pregunta sobre por qué los métodos son mucho más rápidos que los operadores. Mire cuánto más rápidos son ambos métodos PlusEqual que el operador + = equivalente. Y también es interesante cuánto más rápido es - = que + = ... tus tiempos son los primeros en los que he visto eso.
Brian Kennedy
Henk, me interesaría mucho saber la versión del archivo C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll en su sistema ... si es más nuevo que el mío (.3620), pero más antiguo que los de Corey (.5446), entonces eso podría explicar por qué sus operadores siguen siendo lentos como los míos, pero los de Corey no lo son.
Brian Kennedy
1
Solo puedo encontrar la versión .50727, pero no estoy seguro de si es relevante para Fx40 / Fx45.
Henk Holterman
Tienes que ir a Propiedades y hacer clic en la pestaña Versión para ver el resto del número de versión.
Brian Kennedy
2

Además de las diferencias del compilador JIT mencionadas en otras respuestas, otra diferencia entre una llamada al método de estructura y un operador de estructura es que una llamada al método de estructura pasará thiscomo refparámetro (y puede escribirse para aceptar otros parámetros como refparámetros también), mientras que un El operador de estructura pasará todos los operandos por valor. El costo de pasar una estructura de cualquier tamaño como refparámetro es fijo, sin importar cuán grande sea la estructura, mientras que el costo de 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 cuando se utilizan métodos, no se pueden evitar cuando se utilizan operadores.

Super gato
fuente
Hmmm ... bueno, ¡eso podría explicar muchas cosas! Entonces, si el operador es lo suficientemente corto como para estar en línea, supongo que no hará copias innecesarias. Pero si no es así, y su estructura tiene más de una palabra, es posible que no desee implementarla como operador si la velocidad es fundamental. Gracias por esa información.
Brian Kennedy
Por cierto, una cosa que me molesta un poco cuando se responden preguntas sobre la velocidad es "compararlo". Es que tal respuesta ignora el hecho de que en muchos casos lo que importa es si una operación suele tardar 10us o 20us, pero si un ligero cambio de circunstancias puede hacer que tarde 1ms o 10ms. Lo que importa no es qué tan rápido se ejecuta algo en la máquina de un desarrollador, sino si la operación alguna vez será lo suficientemente lenta como para importar ; si el método X se ejecuta dos veces más rápido que el método Y en la mayoría de las máquinas, pero en algunas máquinas será 100 veces más lento, el método Y puede ser la mejor opción.
supercat
Por supuesto, aquí estamos hablando de solo 2 dobles ... no estructuras grandes. Pasar dos dobles en la pila donde se puede acceder a ellos rápidamente no es necesariamente más lento que pasar 'esto' en la pila y luego tener que desreferenciar eso para atraerlos y operar sobre ellos ... pero podría causar diferencias. Sin embargo, en este caso, debería estar en línea, por lo que el Optimizador JIT debería terminar con exactamente el mismo código.
Brian Kennedy
1

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 de mscorwks.dll es 2.0.50727.5446. Simplemente pegué el código en LINQPad y lo ejecuté desde allí. Aquí está 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%.
Daniel Pryden
fuente
2
Interesante ... parece que las optimizaciones que se agregaron al 32b JIT Optimizer aún no han llegado al 64b JIT Optimizer ... sus proporciones siguen siendo muy similares a las mías. Decepcionante ... pero bueno saberlo.
Brian Kennedy
0

Me imagino que cuando accede a los miembros de la estructura, de hecho está haciendo una operación adicional para acceder al miembro, el puntero ESTE + desplazamiento.

Mateo
fuente
1
Bueno, con un objeto de clase, estaría absolutamente en lo cierto ... porque al método simplemente se le pasaría el puntero 'this'. Sin embargo, con las estructuras, eso no debería ser así. La estructura debe pasarse a los métodos de la pila. Por lo tanto, el primer doble debería estar ubicado donde estaría el puntero 'this' y el segundo doble en la posición inmediatamente después ... ambos posiblemente sean registros en la CPU. Por lo tanto, el JIT debería usar un desplazamiento como máximo.
Brian Kennedy
0

¿Puede ser que en lugar de List debería usar double [] con compensaciones e incrementos de índice "bien conocidos"?

Konstantin Isaev
fuente