Ayer encontré un artículo de Christoph Nahr titulado ".NET Struct Performance" que comparó varios lenguajes (C ++, C #, Java, JavaScript) para un método que agrega dos estructuras de puntos ( double
tuplas).
Resultó que la versión de C ++ tarda unos 1000 ms en ejecutarse (1e9 iteraciones), mientras que C # no puede llegar a menos de ~ 3000 ms en la misma máquina (y funciona incluso peor en x64).
Para probarlo yo mismo, tomé el código C # (y lo simplifiqué ligeramente para llamar solo al método donde los parámetros se pasan por valor) y lo ejecuté en una máquina i7-3610QM (aumento de 3.1Ghz para un solo núcleo), 8GB RAM, Win8. 1, usando .NET 4.5.2, RELEASE build de 32 bits (x86 WoW64 ya que mi sistema operativo es de 64 bits). Esta es la versión simplificada:
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Point a = new Point(1, 1), b = new Point(1, 1);
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
}
Con Point
definido como simplemente:
public struct Point
{
private readonly double _x, _y;
public Point(double x, double y) { _x = x; _y = y; }
public double X { get { return _x; } }
public double Y { get { return _y; } }
}
Ejecutarlo produce resultados similares a los del artículo:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Primera observación extraña
Dado que el método debería estar en línea, me pregunté cómo funcionaría el código si eliminara las estructuras por completo y simplemente alineara todo junto:
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
public static void Main()
{
// not using structs at all here
double ax = 1, ay = 1, bx = 1, by = 1;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
{
ax = ax + by;
ay = ay + bx;
}
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
ax, ay, sw.ElapsedMilliseconds);
}
}
Y obtuve prácticamente el mismo resultado (en realidad, un 1% más lento después de varios reintentos), lo que significa que JIT-ter parece estar haciendo un buen trabajo optimizando todas las llamadas a funciones:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
También significa que el punto de referencia no parece medir ningún struct
rendimiento y en realidad solo parece medir la double
aritmética básica (después de que todo lo demás se optimiza).
Las cosas raras
Ahora viene la parte extraña. Si simplemente agrego otro cronómetro fuera del ciclo (sí, lo reduje a este paso loco después de varios reintentos), el código se ejecuta tres veces más rápido :
public static void Main()
{
var outerSw = Stopwatch.StartNew(); // <-- added
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
outerSw.Stop(); // <-- added
}
Result: x=1000000001 y=1000000001, Time elapsed: 961 ms
¡Eso es ridículo! Y no Stopwatch
es que me esté dando resultados incorrectos porque puedo ver claramente que termina después de un solo segundo.
¿Alguien puede decirme qué podría estar pasando aquí?
(Actualizar)
Aquí hay dos métodos en el mismo programa, lo que muestra que la razón no es JITting:
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Test1();
Test2();
Console.WriteLine();
Test1();
Test2();
}
private static void Test1()
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test1: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
private static void Test2()
{
var swOuter = Stopwatch.StartNew();
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
swOuter.Stop();
}
}
Salida:
Test1: x=1000000001 y=1000000001, Time elapsed: 3242 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 974 ms
Test1: x=1000000001 y=1000000001, Time elapsed: 3251 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 972 ms
Aquí hay un pastebin. Debe ejecutarlo como una versión de 32 bits en .NET 4.x (hay un par de comprobaciones en el código para garantizar esto).
(Actualización 4)
Siguiendo los comentarios de @ usr sobre la respuesta de @Hans, verifiqué el desmontaje optimizado para ambos métodos, y son bastante diferentes:
Esto parece mostrar que la diferencia podría deberse a que el compilador actuó de manera extraña en el primer caso, en lugar de a una alineación de doble campo.
Además, si agrego dos variables (desplazamiento total de 8 bytes), todavía obtengo el mismo aumento de velocidad, y ya no parece que esté relacionado con la alineación de campo mencionada por Hans Passant:
// this is still fast?
private static void Test3()
{
var magical_speed_booster_1 = "whatever";
var magical_speed_booster_2 = "whatever";
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
GC.KeepAlive(magical_speed_booster_1);
GC.KeepAlive(magical_speed_booster_2);
}
double
variables locales , no struct
s, así que descarté ineficiencias en el diseño de la estructura / llamada al método.