Wczoraj znalazłem artykuł Christopha Nahra zatytułowany ".NET Struct Performance", w którym porównano kilka języków (C ++, C #, Java, JavaScript) dla metody, która dodaje dwie struktury punktowe ( double
krotki).
Jak się okazało, wykonanie wersji C ++ zajmuje około 1000 ms (iteracje 1e9), podczas gdy C # nie może zejść poniżej ~ 3000 ms na tej samej maszynie (i działa jeszcze gorzej na x64).
Aby samemu to przetestować, wziąłem kod C # (i nieco uprościłem, aby wywołać tylko metodę, w której parametry są przekazywane przez wartość) i uruchomiłem go na maszynie i7-3610QM (3,1 GHz doładowanie dla pojedynczego rdzenia), 8 GB pamięci RAM, Win8. 1, przy użyciu .NET 4.5.2, wersja RELEASE 32-bitowa (x86 WoW64, ponieważ mój system operacyjny jest 64-bitowy). To jest wersja uproszczona:
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);
}
}
Z Point
definicją po prostu:
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; } }
}
Uruchomienie go daje wyniki podobne do tych w artykule:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Pierwsza dziwna obserwacja
Ponieważ metoda powinna być wbudowana, zastanawiałem się, jak działałby kod, gdybym całkowicie usunął struktury i po prostu wstawił całość razem:
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);
}
}
I uzyskał praktycznie ten sam wynik (właściwie 1% wolniej po kilku próbach), co oznacza, że JIT-ter wydaje się wykonywać dobrą robotę optymalizując wszystkie wywołania funkcji:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Oznacza to również, że benchmark nie wydaje się mierzyć żadnej struct
wydajności i wydaje się mierzyć tylko podstawową double
arytmetykę (po optymalizacji wszystkiego innego).
Dziwne rzeczy
Teraz czas na dziwną część. Jeśli po prostu dodam kolejny stoper poza pętlą (tak, zawęziłem to do tego szalonego kroku po kilku próbach), kod działa trzy razy szybciej :
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
To niedorzeczne! I to nie Stopwatch
jest tak, że daje mi złe wyniki, ponieważ wyraźnie widzę, że kończy się po jednej sekundzie.
Czy ktoś może mi powiedzieć, co się tutaj dzieje?
(Aktualizacja)
Oto dwie metody w tym samym programie, które pokazują, że przyczyną nie jest JIT:
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();
}
}
Wynik:
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
Oto pastebin. Musisz go uruchomić w wersji 32-bitowej na platformie .NET 4.x (w kodzie znajduje się kilka elementów sprawdzających, aby to zapewnić).
(Aktualizacja 4)
Po komentarzach @ usr na temat odpowiedzi @Hansa sprawdziłem zoptymalizowany demontaż dla obu metod i są one raczej różne:
To wydaje się pokazywać, że różnica może wynikać z dziwnego działania kompilatora w pierwszym przypadku, a nie z wyrównania podwójnego pola?
Ponadto, jeśli dodam dwie zmienne (całkowite przesunięcie 8 bajtów), nadal otrzymuję to samo przyspieszenie - i nie wydaje się, że jest to związane z wyrównaniem pola, o którym wspomniał 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);
}