Ontem encontrei um artigo de Christoph Nahr intitulado ".NET Struct Performance" que comparou várias linguagens (C ++, C #, Java, JavaScript) para um método que adiciona duas estruturas de pontos ( double
tuplas).
Como se viu, a versão C ++ leva cerca de 1000 ms para executar (iterações 1e9), enquanto C # não pode ficar abaixo de ~ 3000 ms na mesma máquina (e tem desempenho ainda pior em x64).
Para testar eu mesmo, peguei o código C # (e simplifiquei um pouco para chamar apenas o método onde os parâmetros são passados por valor) e o executei em uma máquina i7-3610QM (aumento de 3,1 GHz para núcleo único), 8 GB de RAM, Win8. 1, usando .NET 4.5.2, RELEASE build de 32 bits (x86 WoW64, pois meu sistema operacional é de 64 bits). Esta é a versão 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);
}
}
Com Point
definido como simplesmente:
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; } }
}
Executá-lo produz resultados semelhantes aos do artigo:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Primeira observação estranha
Uma vez que o método deve ser embutido, eu me perguntei como o código funcionaria se eu removesse totalmente as estruturas e simplesmente colocasse tudo embutido juntos:
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);
}
}
E obteve praticamente o mesmo resultado (na verdade, 1% mais lento após várias tentativas), o que significa que o JIT-ter parece estar fazendo um bom trabalho otimizando todas as chamadas de função:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Também significa que o benchmark não parece medir nenhum struct
desempenho e, na verdade, parece medir apenas a double
aritmética básica (depois que todo o resto for otimizado).
As coisas estranhas
Agora vem a parte estranha. Se eu simplesmente adicionar outro cronômetro fora do loop (sim, reduzi-o a esta etapa maluca após várias tentativas), o código será executado três vezes mais 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
Isso é ridículo! E não Stopwatch
é como se estivesse me dando resultados errados, porque posso ver claramente que termina após um único segundo.
Alguém pode me dizer o que pode estar acontecendo aqui?
(Atualizar)
Aqui estão dois métodos no mesmo programa, o que mostra que o motivo não é 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();
}
}
Resultado:
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
Aqui está um pastebin. Você precisa executá-lo como uma versão de 32 bits no .NET 4.x (há algumas verificações no código para garantir isso).
(Atualização 4)
Seguindo os comentários de @usr sobre a resposta de @Hans, verifiquei a desmontagem otimizada para ambos os métodos, e eles são bastante diferentes:
Isso parece mostrar que a diferença pode ser devido ao compilador estar agindo de forma estranha no primeiro caso, ao invés do alinhamento de campo duplo.
Além disso, se eu adicionar duas variáveis (deslocamento total de 8 bytes), ainda obtenho o mesmo aumento de velocidade - e não parece mais que esteja relacionado à menção de alinhamento de campo 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
variáveis locais , sem struct
s, então descartei as ineficiências de layout de estrutura / chamada de método.