Вчера я нашел статью Кристофа Нара под названием «.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
значит, что это дает мне неправильные результаты, потому что я ясно вижу, что это заканчивается через одну секунду.
Кто-нибудь может сказать мне, что здесь может происходить?
(Обновить)
Вот два метода в одной программе, которые показывают, что причина не в 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();
}
}
Вывод:
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);
}