Gestern habe ich einen Artikel von Christoph Nahr mit dem Titel ".NET Struct Performance" gefunden, in dem mehrere Sprachen (C ++, C #, Java, JavaScript) für eine Methode verglichen wurden, die zwei Punktstrukturen ( double
Tupel) hinzufügt .
Wie sich herausstellte, dauert die Ausführung der C ++ - Version etwa 1000 ms (1e9-Iterationen), während C # auf demselben Computer nicht unter ~ 3000 ms geraten kann (und in x64 sogar noch schlechter abschneidet).
Um es selbst zu testen, nahm ich den C # -Code (und vereinfachte ihn leicht, um nur die Methode aufzurufen, bei der Parameter als Wert übergeben werden) und führte ihn auf einem i7-3610QM-Computer (3,1 GHz Boost für Single Core), 8 GB RAM, Win8 aus. 1, mit .NET 4.5.2, RELEASE Build 32-Bit (x86 WoW64, da mein Betriebssystem 64-Bit ist). Dies ist die vereinfachte Version:
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);
}
}
Mit Point
definiert als einfach:
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; } }
}
Wenn Sie es ausführen, werden ähnliche Ergebnisse wie im Artikel erzielt:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Erste seltsame Beobachtung
Da die Methode inline sein sollte, fragte ich mich, wie sich der Code verhalten würde, wenn ich Strukturen vollständig entfernen und einfach das Ganze zusammen inlinieren würde:
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);
}
}
Und hat praktisch das gleiche Ergebnis erzielt (tatsächlich 1% langsamer nach mehreren Wiederholungsversuchen), was bedeutet, dass JIT-ter gute Arbeit bei der Optimierung aller Funktionsaufrufe zu leisten scheint:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Dies bedeutet auch, dass der Benchmark keine struct
Leistung zu messen scheint und tatsächlich nur die Basis zu messen scheintdouble
(nachdem alles andere wegoptimiert wurde).
Das seltsame Zeug
Jetzt kommt der seltsame Teil. Wenn ich lediglich eine weitere Stoppuhr außerhalb der Schleife hinzufüge (ja, ich habe sie nach mehreren Wiederholungsversuchen auf diesen verrückten Schritt eingegrenzt), wird der Code dreimal schneller ausgeführt :
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
Das ist lächerlich! Und es ist nicht soStopwatch
, als würde ich falsche Ergebnisse erzielen, weil ich deutlich sehen kann, dass es nach einer einzigen Sekunde endet.
Kann mir jemand sagen, was hier passieren könnte?
(Aktualisieren)
Hier sind zwei Methoden im selben Programm, die zeigen, dass der Grund nicht JITting ist:
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();
}
}
Ausgabe:
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
Hier ist ein Pastebin. Sie müssen es als 32-Bit-Version unter .NET 4.x ausführen (der Code enthält einige Überprüfungen, um dies sicherzustellen).
(Update 4)
Nach den Kommentaren von @ usr zur Antwort von @Hans habe ich die optimierte Demontage für beide Methoden überprüft und sie sind ziemlich unterschiedlich:
Dies scheint zu zeigen, dass der Unterschied möglicherweise darauf zurückzuführen ist, dass der Compiler im ersten Fall lustig handelt und nicht auf eine doppelte Feldausrichtung.
Wenn ich zwei Variablen hinzufüge (Gesamtversatz von 8 Bytes), erhalte ich immer noch den gleichen Geschwindigkeitsschub - und es scheint nicht mehr, dass dies mit der von Hans Passant erwähnten Feldausrichtung zusammenhängt:
// 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
Variablen durchgeführt, nein struct
s, also habe ich Ineffizienzen beim Strukturlayout / Methodenaufruf ausgeschlossen.