소개 : C #으로 고성능 코드를 작성합니다. 예, C ++가 더 나은 최적화를 제공한다는 것을 알고 있지만 여전히 C #을 사용하기로 선택합니다. 나는 그 선택에 대해 토론하고 싶지 않습니다. 오히려 저와 같은 .NET Framework에서 고성능 코드를 작성하려는 사람들의 의견을 듣고 싶습니다.
질문 :
- 아래 코드의 연산자가 동등한 메서드 호출보다 느린 이유는 무엇입니까 ??
- 아래 코드에서 두 개의 double을 전달하는 메서드가 내부에 두 개의 double이있는 구조체를 전달하는 동등한 메서드보다 빠른 이유는 무엇입니까? (A : 오래된 JIT는 구조를 제대로 최적화하지 못합니다)
- .NET JIT 컴파일러가 간단한 구조체를 구조체의 멤버만큼 효율적으로 처리하도록하는 방법이 있습니까? (A : 최신 JIT 받기)
내가 아는 것 :
원래 .NET JIT 컴파일러는 구조체와 관련된 어떤 것도 인라인하지 않습니다. 기괴한 주어진 구조체는 내장과 같이 최적화되어야하지만 사실 인 작은 값 유형이 필요한 경우에만 사용해야합니다. 다행히도 .NET 3.5SP1 및 .NET 2.0SP2에서는 특히 구조체에 대한 인라인 개선을 포함하여 JIT Optimizer를 일부 개선했습니다. (그렇지 않으면 그들이 도입 한 새로운 Complex 구조체가 끔찍하게 수행되었을 것이기 때문에 그렇게했다고 생각합니다 … 그래서 Complex 팀은 아마도 JIT Optimizer 팀을들이 받았을 것입니다.) 따라서 .NET 3.5 SP1 이전의 모든 문서는 아마도 이 문제와 너무 관련이 없습니다.
내 테스트 결과 :
C : \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll 파일에 버전> = 3053이 있는지 확인하여 최신 JIT Optimizer가 있는지 확인했습니다. JIT 최적화 프로그램에. 그러나 그것으로도 분해에 대한 나의 타이밍과 모습은 다음과 같습니다.
두 개의 double이있는 구조체를 전달하기위한 JIT 생성 코드는 두 개의 double을 직접 전달하는 코드보다 훨씬 덜 효율적입니다.
구조체 메서드에 대한 JIT 생성 코드는 구조체를 인수로 전달하는 것보다 훨씬 효율적으로 ‘this’를 전달합니다.
루프에 명확하게 있기 때문에 승수를 사용하더라도 두 개의 double이있는 구조체를 전달하는 것보다 두 개의 double을 전달하는 경우 JIT는 여전히 더 잘 인라인됩니다.
타이밍 :
사실, 분해를 살펴보면 루프에서 대부분의 시간이 목록에서 테스트 데이터에 액세스하는 것임을 알고 있습니다. 루프의 오버 헤드 코드와 데이터 액세스를 고려하면 동일한 호출을 만드는 네 가지 방법의 차이가 크게 다릅니다. PlusEqual (Element) 대신 PlusEqual (double, double)을 수행하면 속도가 5 배에서 20 배까지 향상됩니다. + = 연산자 대신 PlusEqual (double, double)을 수행하는 경우 10x ~ 40x. 와. 슬퍼.
다음은 한 세트의 타이밍입니다.
Populating List<Element> took 320ms.
The PlusEqual() method took 105ms.
The 'same' += operator took 131ms.
The 'same' -= operator took 139ms.
The PlusEqual(double, double) method took 68ms.
The do nothing loop took 66ms.
The ratio of operator with constructor to method is 124%.
The ratio of operator without constructor to method is 132%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 64%.
If we remove the overhead time for the loop accessing the elements from the List...
The ratio of operator with constructor to method is 166%.
The ratio of operator without constructor to method is 187%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 5%.
코드:
namespace OperatorVsMethod
{
public struct Element
{
public double Left;
public double Right;
public Element(double left, double right)
{
this.Left = left;
this.Right = right;
}
public static Element operator +(Element x, Element y)
{
return new Element(x.Left + y.Left, x.Right + y.Right);
}
public static Element operator -(Element x, Element y)
{
x.Left += y.Left;
x.Right += y.Right;
return x;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(Element that)
{
this.Left += that.Left;
this.Right += that.Right;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(double thatLeft, double thatRight)
{
this.Left += thatLeft;
this.Right += thatRight;
}
}
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
Stopwatch stopwatch = new Stopwatch();
// Populate a List of Elements to multiply together
int seedSize = 4;
List<double> doubles = new List<double>(seedSize);
doubles.Add(2.5d);
doubles.Add(100000d);
doubles.Add(-0.5d);
doubles.Add(-100002d);
int size = 2500000 * seedSize;
List<Element> elts = new List<Element>(size);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
int di = ii % seedSize;
double d = doubles[di];
elts.Add(new Element(d, d));
}
stopwatch.Stop();
long populateMS = stopwatch.ElapsedMilliseconds;
// Measure speed of += operator (calls ctor)
Element operatorCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorCtorResult += elts[ii];
}
stopwatch.Stop();
long operatorCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of -= operator (+= without ctor)
Element operatorNoCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorNoCtorResult -= elts[ii];
}
stopwatch.Stop();
long operatorNoCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(Element) method
Element plusEqualResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
plusEqualResult.PlusEqual(elts[ii]);
}
stopwatch.Stop();
long plusEqualMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(double, double) method
Element plusEqualDDResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
plusEqualDDResult.PlusEqual(elt.Left, elt.Right);
}
stopwatch.Stop();
long plusEqualDDMS = stopwatch.ElapsedMilliseconds;
// Measure speed of doing nothing but accessing the Element
Element doNothingResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
double left = elt.Left;
double right = elt.Right;
}
stopwatch.Stop();
long doNothingMS = stopwatch.ElapsedMilliseconds;
// Report results
Assert.AreEqual(1d, operatorCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, operatorNoCtorResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, plusEqualDDResult.Left, "The operator += did not compute the right result!");
Assert.AreEqual(1d, doNothingResult.Left, "The operator += did not compute the right result!");
// Report speeds
Console.WriteLine("Populating List<Element> took {0}ms.", populateMS);
Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS);
Console.WriteLine("The 'same' += operator took {0}ms.", operatorCtorMS);
Console.WriteLine("The 'same' -= operator took {0}ms.", operatorNoCtorMS);
Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS);
Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS);
// Compare speeds
long percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
operatorCtorMS -= doNothingMS;
operatorNoCtorMS -= doNothingMS;
plusEqualMS -= doNothingMS;
plusEqualDDMS -= doNothingMS;
Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List...");
percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
}
}
}
IL : (일명 위의 일부가 컴파일되는 내용)
public void PlusEqual(Element that)
{
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,30h
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
00000013 mov dword ptr [ebp-3Ch],ecx
00000016 cmp dword ptr ds:[04C87B7Ch],0
0000001d je 00000024
0000001f call 753081B1
00000024 nop
this.Left += that.Left;
00000025 mov eax,dword ptr [ebp-3Ch]
00000028 fld qword ptr [ebp+8]
0000002b fadd qword ptr [eax]
0000002d fstp qword ptr [eax]
this.Right += that.Right;
0000002f mov eax,dword ptr [ebp-3Ch]
00000032 fld qword ptr [ebp+10h]
00000035 fadd qword ptr [eax+8]
00000038 fstp qword ptr [eax+8]
}
0000003b nop
0000003c lea esp,[ebp-0Ch]
0000003f pop ebx
00000040 pop esi
00000041 pop edi
00000042 pop ebp
00000043 ret 10h
public void PlusEqual(double thatLeft, double thatRight)
{
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,30h
00000009 xor eax,eax
0000000b mov dword ptr [ebp-10h],eax
0000000e xor eax,eax
00000010 mov dword ptr [ebp-1Ch],eax
00000013 mov dword ptr [ebp-3Ch],ecx
00000016 cmp dword ptr ds:[04C87B7Ch],0
0000001d je 00000024
0000001f call 75308159
00000024 nop
this.Left += thatLeft;
00000025 mov eax,dword ptr [ebp-3Ch]
00000028 fld qword ptr [ebp+10h]
0000002b fadd qword ptr [eax]
0000002d fstp qword ptr [eax]
this.Right += thatRight;
0000002f mov eax,dword ptr [ebp-3Ch]
00000032 fld qword ptr [ebp+8]
00000035 fadd qword ptr [eax+8]
00000038 fstp qword ptr [eax+8]
}
0000003b nop
0000003c lea esp,[ebp-0Ch]
0000003f pop ebx
00000040 pop esi
00000041 pop edi
00000042 pop ebp
00000043 ret 10h
답변
나는 매우 다른 결과를 얻고 있지만 훨씬 덜 극적입니다. 하지만 테스트 실행기를 사용하지 않고 코드를 콘솔 모드 앱에 붙여 넣었습니다. 5 % 결과는 32 비트 모드에서 ~ 87 %, 시도하면 64 비트 모드에서 ~ 100 %입니다.
정렬은 double에서 중요하며 .NET 런타임은 32 비트 시스템에서 4의 정렬 만 약속 할 수 있습니다. 나에게 테스트 러너는 8 대신 4로 정렬 된 스택 주소로 테스트 메서드를 시작하는 것 같습니다. 이중 정렬이 캐시 라인 경계를 넘어갈 때 정렬 불량 패널티가 매우 커집니다.
답변
결과를 복제하는 데 어려움이 있습니다.
나는 당신의 코드를 가져 왔습니다.
- 독립형 콘솔 응용 프로그램으로 만들었습니다.
- 최적화 된 (릴리스) 빌드 빌드
- “크기”요소를 2.5M에서 10M으로 증가
- 명령 줄에서 실행 (IDE 외부)
그렇게했을 때 다음과 같은 타이밍이 당신의 타이밍과 크게 다릅니다. 의심의 여지를 없애기 위해 내가 사용한 코드를 정확하게 게시하겠습니다.
내 타이밍은 다음과 같습니다.
Populating List<Element> took 527ms.
The PlusEqual() method took 450ms.
The 'same' += operator took 386ms.
The 'same' -= operator took 446ms.
The PlusEqual(double, double) method took 413ms.
The do nothing loop took 229ms.
The ratio of operator with constructor to method is 85%.
The ratio of operator without constructor to method is 99%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 91%.
If we remove the overhead time for the loop accessing the elements from the List...
The ratio of operator with constructor to method is 71%.
The ratio of operator without constructor to method is 98%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 83%.
그리고 이것은 당신의 코드에 대한 나의 편집입니다.
namespace OperatorVsMethod
{
public struct Element
{
public double Left;
public double Right;
public Element(double left, double right)
{
this.Left = left;
this.Right = right;
}
public static Element operator +(Element x, Element y)
{
return new Element(x.Left + y.Left, x.Right + y.Right);
}
public static Element operator -(Element x, Element y)
{
x.Left += y.Left;
x.Right += y.Right;
return x;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(Element that)
{
this.Left += that.Left;
this.Right += that.Right;
}
/// <summary>
/// Like the += operator; but faster.
/// </summary>
public void PlusEqual(double thatLeft, double thatRight)
{
this.Left += thatLeft;
this.Right += thatRight;
}
}
public class UnitTest1
{
public static void Main()
{
Stopwatch stopwatch = new Stopwatch();
// Populate a List of Elements to multiply together
int seedSize = 4;
List<double> doubles = new List<double>(seedSize);
doubles.Add(2.5d);
doubles.Add(100000d);
doubles.Add(-0.5d);
doubles.Add(-100002d);
int size = 10000000 * seedSize;
List<Element> elts = new List<Element>(size);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
int di = ii % seedSize;
double d = doubles[di];
elts.Add(new Element(d, d));
}
stopwatch.Stop();
long populateMS = stopwatch.ElapsedMilliseconds;
// Measure speed of += operator (calls ctor)
Element operatorCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorCtorResult += elts[ii];
}
stopwatch.Stop();
long operatorCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of -= operator (+= without ctor)
Element operatorNoCtorResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
operatorNoCtorResult -= elts[ii];
}
stopwatch.Stop();
long operatorNoCtorMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(Element) method
Element plusEqualResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
plusEqualResult.PlusEqual(elts[ii]);
}
stopwatch.Stop();
long plusEqualMS = stopwatch.ElapsedMilliseconds;
// Measure speed of PlusEqual(double, double) method
Element plusEqualDDResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
plusEqualDDResult.PlusEqual(elt.Left, elt.Right);
}
stopwatch.Stop();
long plusEqualDDMS = stopwatch.ElapsedMilliseconds;
// Measure speed of doing nothing but accessing the Element
Element doNothingResult = new Element(1d, 1d);
stopwatch.Reset();
stopwatch.Start();
for (int ii = 0; ii < size; ++ii)
{
Element elt = elts[ii];
double left = elt.Left;
double right = elt.Right;
}
stopwatch.Stop();
long doNothingMS = stopwatch.ElapsedMilliseconds;
// Report speeds
Console.WriteLine("Populating List<Element> took {0}ms.", populateMS);
Console.WriteLine("The PlusEqual() method took {0}ms.", plusEqualMS);
Console.WriteLine("The 'same' += operator took {0}ms.", operatorCtorMS);
Console.WriteLine("The 'same' -= operator took {0}ms.", operatorNoCtorMS);
Console.WriteLine("The PlusEqual(double, double) method took {0}ms.", plusEqualDDMS);
Console.WriteLine("The do nothing loop took {0}ms.", doNothingMS);
// Compare speeds
long percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
operatorCtorMS -= doNothingMS;
operatorNoCtorMS -= doNothingMS;
plusEqualMS -= doNothingMS;
plusEqualDDMS -= doNothingMS;
Console.WriteLine("If we remove the overhead time for the loop accessing the elements from the List...");
percentageRatio = 100L * operatorCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator with constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * operatorNoCtorMS / plusEqualMS;
Console.WriteLine("The ratio of operator without constructor to method is {0}%.", percentageRatio);
percentageRatio = 100L * plusEqualDDMS / plusEqualMS;
Console.WriteLine("The ratio of PlusEqual(double,double) to PlusEqual(Element) is {0}%.", percentageRatio);
}
}
}
답변
여기서 .NET 4.0을 실행합니다. 릴리스 모드에서 .NET 4.0을 대상으로 “모든 CPU”로 컴파일했습니다. 실행은 명령 줄에서 이루어졌습니다. 64 비트 모드에서 실행되었습니다. 제 타이밍이 조금 다릅니다.
Populating List<Element> took 442ms.
The PlusEqual() method took 115ms.
The 'same' += operator took 201ms.
The 'same' -= operator took 200ms.
The PlusEqual(double, double) method took 129ms.
The do nothing loop took 93ms.
The ratio of operator with constructor to method is 174%.
The ratio of operator without constructor to method is 173%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 112%.
If we remove the overhead time for the loop accessing the elements from the List
...
The ratio of operator with constructor to method is 490%.
The ratio of operator without constructor to method is 486%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 163%.
특히 PlusEqual(Element)
는보다 약간 빠릅니다 PlusEqual(double, double)
.
.NET 3.5에서 문제가 무엇이든 .NET 4.0에는 존재하지 않는 것 같습니다.
답변
@Corey Kosak과 마찬가지로 VS 2010 Express에서 릴리스 모드의 간단한 콘솔 앱으로이 코드를 실행했습니다. 나는 매우 다른 숫자를 얻습니다. 그러나 Fx4.5도 있으므로 깨끗한 Fx4.0의 결과가 아닐 수도 있습니다.
Populating List<Element> took 435ms.
The PlusEqual() method took 109ms.
The 'same' += operator took 217ms.
The 'same' -= operator took 157ms.
The PlusEqual(double, double) method took 118ms.
The do nothing loop took 79ms.
The ratio of operator with constructor to method is 199%.
The ratio of operator without constructor to method is 144%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 108%.
If we remove the overhead time for the loop accessing the elements from the List
...
The ratio of operator with constructor to method is 460%.
The ratio of operator without constructor to method is 260%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 130%.
편집 : 이제 cmd 라인에서 실행합니다. 그것은 차이를 만들고 숫자의 변동을 줄입니다.
답변
다른 답변에서 언급 된 JIT 컴파일러 차이점 외에도 struct 메서드 호출과 struct 연산자의 또 다른 차이점은 struct 메서드 호출이 통과한다는 것입니다. this
ref
매개 변수 로 되고 다른 매개 변수를 ref
매개 변수로 받아들이도록 작성 될 수 있다는 것입니다 . 구조체 연산자는 모든 피연산자를 값으로 전달합니다. 어떤 크기의 구조를 ref
매개 변수 로 전달하는 비용은 구조의 크기에 관계없이 고정되어있는 반면, 더 큰 구조를 전달하는 비용은 구조 크기에 비례합니다. 불필요하게 복사하는 것을 피할 수 있다면 큰 구조 (수백 바이트라도)를 사용하는 것은 잘못된 것이 아닙니다 . 방법을 사용하면 불필요한 복사를 방지 할 수 있지만 연산자를 사용하면 방지 할 수 없습니다.
답변
이것이 관련성이 있는지 확실하지 않지만 다음은 Windows 7 64 비트에서 .NET 4.0 64 비트에 대한 수치입니다. 내 mscorwks.dll 버전은 2.0.50727.5446입니다. 방금 코드를 LINQPad에 붙여넣고 거기에서 실행했습니다. 결과는 다음과 같습니다.
Populating List<Element> took 496ms.
The PlusEqual() method took 189ms.
The 'same' += operator took 295ms.
The 'same' -= operator took 358ms.
The PlusEqual(double, double) method took 148ms.
The do nothing loop took 103ms.
The ratio of operator with constructor to method is 156%.
The ratio of operator without constructor to method is 189%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 78%.
If we remove the overhead time for the loop accessing the elements from the List
...
The ratio of operator with constructor to method is 223%.
The ratio of operator without constructor to method is 296%.
The ratio of PlusEqual(double,double) to PlusEqual(Element) is 52%.
답변
구조체의 멤버에 액세스 할 때 실제로 멤버에 액세스하는 추가 작업 인 THIS 포인터 + 오프셋을 수행하는 것처럼 상상할 수 있습니다.