어제 저는 Christoph Nahr의 ".NET Struct Performance"라는 기사를 발견했습니다.이 기사 는 2 포인트 구조체 ( double
튜플) 를 추가하는 방법에 대해 여러 언어 (C ++, C #, Java, JavaScript)를 벤치마킹했습니다 .
결과적으로 C ++ 버전은 실행하는 데 약 1000ms (1e9 반복)가 걸리는 반면 C #은 동일한 컴퓨터에서 ~ 3000ms 미만으로 도달 할 수 없으며 x64에서는 더 나빠집니다.
직접 테스트하기 위해 C # 코드 (그리고 매개 변수가 값으로 전달되는 메서드 만 호출하기 위해 약간 단순화 됨)를 가져와 i7-3610QM 시스템 (단일 코어의 경우 3.1Ghz 부스트), 8GB RAM, Win8에서 실행했습니다. 1, .NET 4.5.2 사용, RELEASE 빌드 32 비트 (내 OS가 64 비트이므로 x86 WoW64). 이것은 단순화 된 버전입니다.
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
1 초 후에 끝나는 것을 분명히 볼 수 있기 때문에 잘못된 결과를주는 것과는 다릅니다.
아무도 여기서 무슨 일이 일어날 지 말해 줄 수 있습니까?
(최신 정보)
다음은 동일한 프로그램에있는 두 가지 방법으로, 이유가 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
여기 페이스트 빈이 있습니다. .NET 4.x에서 32 비트 릴리스로 실행해야합니다 (이를 확인하기 위해 코드에 몇 가지 검사가 있습니다).
(업데이트 4)
@Hans의 답변에 대한 @usr의 의견에 따라 두 가지 방법에 대해 최적화 된 분해를 확인했으며 다소 다릅니다.
이것은 컴파일러가 이중 필드 정렬이 아닌 첫 번째 경우에 재미있게 행동하기 때문일 수 있음을 보여주는 것 같습니다.
또한 두 개의 변수 (총 오프셋 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);
}