Вчера я нашел статью Кристофа Нахра озаглавленную "Производительность .NET Struct" , в которой сравнивались несколько языков (С++, С#, Java, JavaScript) для метода, который добавляет две точечные структуры (double
кортежи).
Как оказалось, версия С++ занимает около 1000 мс для выполнения (1е9 итераций), в то время как С# не может получить до ~ 3000 мс на одной машине (и еще хуже в x64).
Чтобы проверить его сам, я взял код С# (и немного упростил вызов только метода, когда параметры переданы по значению), и запустил его на машине 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 байтов), я все равно получаю такое же ускорение скорости - и это уже не похоже на упоминание выравнивания полей 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);
}