Ieri ho trovato un articolo di Christoph Nahr intitolato ".NET Struct Performance" che confrontava diversi linguaggi (C ++, C #, Java, JavaScript) per un metodo che aggiunge strutture a due punti ( double
tuple).
Come si è scoperto, la versione C ++ impiega circa 1000 ms per essere eseguita (iterazioni 1e9), mentre C # non può scendere sotto i ~ 3000 ms sulla stessa macchina (e funziona anche peggio in x64).
Per testarlo da solo, ho preso il codice C # (e semplificato leggermente per chiamare solo il metodo in cui i parametri vengono passati per valore) e l'ho eseguito su una macchina i7-3610QM (boost di 3.1Ghz per single core), 8 GB di RAM, Win8. 1, utilizzando .NET 4.5.2, RELEASE build a 32 bit (x86 WoW64 poiché il mio sistema operativo è a 64 bit). Questa è la versione semplificata:
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
definito semplicemente:
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; } }
}
L'esecuzione produce risultati simili a quelli dell'articolo:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Prima strana osservazione
Poiché il metodo dovrebbe essere inline, mi chiedevo come si sarebbe comportato il codice se avessi rimosso completamente le strutture e semplicemente inline l'intera cosa insieme:
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 ha ottenuto praticamente lo stesso risultato (in realtà l'1% più lento dopo diversi tentativi), il che significa che JIT-ter sembra fare un buon lavoro ottimizzando tutte le chiamate di funzione:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Significa anche che il benchmark non sembra misurare alcuna struct
prestazione e in realtà sembra misurare solo l' double
aritmetica di base (dopo che tutto il resto è stato ottimizzato).
Le cose strane
Adesso arriva la parte strana. Se aggiungo semplicemente un altro cronometro fuori dal ciclo (sì, l'ho ristretto a questo passaggio folle dopo diversi tentativi), il codice viene eseguito tre volte più velocemente :
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
È ridicolo! E non Stopwatch
è che mi stia dando risultati sbagliati perché posso vedere chiaramente che finisce dopo un solo secondo.
Qualcuno può dirmi cosa potrebbe succedere qui?
(Aggiornare)
Ecco due metodi nello stesso programma, che mostra che il motivo non è 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();
}
}
Produzione:
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
Ecco un pastebin. È necessario eseguirlo come una versione a 32 bit su .NET 4.x (ci sono un paio di controlli nel codice per garantire questo).
(Aggiorna 4)
Seguendo i commenti di @usr sulla risposta di @Hans, ho controllato lo smontaggio ottimizzato per entrambi i metodi, e sono piuttosto diversi:
Questo sembra dimostrare che la differenza potrebbe essere dovuta al fatto che il compilatore si comporta in modo strano nel primo caso, piuttosto che al doppio allineamento dei campi?
Inoltre, se aggiungo due variabili (offset totale di 8 byte), ottengo comunque lo stesso aumento di velocità e non sembra più correlato alla menzione dell'allineamento del campo di 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
variabili locali , no struct
s, quindi ho escluso le inefficienze di struttura / chiamata del metodo.