Вчора я знайшов статтю Крістофа Нара під назвою ".NET Struct Performance", яка тестувала кілька мов (C ++, C #, Java, JavaScript) для методу, який додає дві точкові структури ( double
кортежі).
Як виявилося, для виконання версії C ++ потрібно близько 1000 мс (ітерацій 1e9), тоді як C # не може отримати менше ~ 3000 мс на тій самій машині (і працює ще гірше в x64).
Щоб протестувати його сам, я взяв код C # (і трохи спростив, щоб викликати лише метод, де параметри передаються за значенням), і запустив його на машині i7-3610QM (посилення 3,1 ГГц для одноядерного), 8 ГБ оперативної пам'яті, Win8. 1, використовуючи .NET 4.5.2, RELEASE build 32-bit (x86 WoW64, оскільки моя ОС 64-бітна). Це спрощена версія:
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);
}
}
З Point
визначається як просто:
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; } }
}
Запустивши його, ви отримаєте результати, подібні до тих, що в статті:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Перше дивне спостереження
Оскільки метод повинен бути вбудований, я подумав, як би діяв код, якщо я взагалі видалюю структури і просто вкладаю все разом:
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);
}
}
І отримав практично однаковий результат (насправді на 1% повільніший після декількох спроб), що означає, що JIT-ter, здається, робить хорошу роботу, оптимізуючи всі виклики функцій:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Це також означає, що контрольний показник, здається, не вимірює жодної struct
продуктивності, а насправді, здається, вимірює лише базову double
арифметику (адже все інше оптимізується).
Дивні речі
Тепер настає дивна частина. Якщо я просто додаю ще один секундомір поза циклом (так, я звузив його до цього шаленого кроку після кількох спроб), код працює втричі швидше :
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
Це смішно! І це не так, як Stopwatch
це дає мені неправильні результати, тому що я чітко бачу, що це закінчується через одну секунду.
Хтось може сказати мені, що тут може відбуватися?
(Оновлення)
Ось два методи в одній програмі, що показує, що причина не в 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();
}
}
Вихід:
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
Ось пастебін. Вам потрібно запустити його як 32-розрядний випуск на .NET 4.x (у коді є кілька перевірок, щоб це переконатись).
(Оновлення 4)
Слідом за коментарями @ usr щодо відповіді @Hans, я перевірив оптимізований розбір для обох методів, і вони досить різні:
Це, мабуть, показує, що різниця може бути пов’язана з компілятором, який у першому випадку поводиться смішно, а не з подвійним вирівнюванням поля?
Крім того, якщо я додаю дві змінні (загальний зсув 8 байт), я все одно отримую однаковий приріст швидкості - і це більше не здається, що це пов'язано із вирівнюванням полів Гансом Пассантом:
// 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
змінними, без struct
s, тому я виключив неефективність виклику структури / методу.