연산자가 메서드 호출보다 훨씬 느린 이유는 무엇입니까? (구조는 이전 JIT에서만 느립니다)


84

소개 : 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 

22
와우, 이것은 Stackoverflow에 대한 좋은 질문이 어떻게 보일 수 있는지에 대한 예제로 참조되어야합니다! 자동 생성 된 주석 만 생략 할 수 있습니다. 불행히도 나는 실제로 문제에 뛰어 들기에는 너무 적지 만 질문이 정말 마음에 듭니다!
Dennis Traub 2011 년

2
단위 테스트가 벤치 마크를 실행하기에 좋은 곳이라고 생각하지 않습니다.
Henk Holterman 2011 년

1
왜 구조체가 두 배보다 빨라야합니까? .NET에서 구조체는 멤버 크기의 합과 같지 않습니다. 따라서 정의에 따라 더 크므로 정의에 따라 스택을 푸시 할 때 더 느려 야하고 두 개의 이중 값만 있어야합니다. 컴파일러가 행 2 이중 메모리에서 구조체 매개 변수를 인라인하는 경우 메서드 내부에서 리플렉션을 사용하여 해당 구조체에 액세스하려는 경우 어떻게 될까요? 해당 구조체 객체에 연결된 런타임 정보는 어디에 있습니까? 그렇지 않나요, 아니면 뭔가 빠졌나요?
Tigran 2011 년

3
@Tigran : 그러한 주장에 대한 출처가 필요합니다. 나는 당신이 틀렸다고 생각합니다. 값 유형이 박스로 표시되는 경우에만 메타 데이터를 값과 함께 저장해야합니다. 정적 구조체 유형의 변수에는 오버 헤드가 없습니다.
Ben Voigt 2011 년

1
빠진 것은 어셈블리 뿐이라고 생각했습니다. 이제 추가했습니다 (MSIL이 아니라 x86 어셈블러입니다).
Ben Voigt 2011 년

답변:


9

나는 매우 다른 결과를 얻고 있지만 훨씬 덜 극적입니다. 하지만 테스트 실행기를 사용하지 않고 코드를 콘솔 모드 앱에 붙여 넣었습니다. 5 % 결과는 32 비트 모드에서 ~ 87 %, 시도하면 64 비트 모드에서 ~ 100 %입니다.

정렬은 double에서 중요하며 .NET 런타임은 32 비트 시스템에서 4의 정렬 만 약속 할 수 있습니다. 나에게 테스트 러너는 8 대신 4로 정렬 된 스택 주소로 테스트 메서드를 시작하는 것 같습니다. 이중 정렬이 캐시 라인 경계를 넘어갈 때 정렬 불량 패널티가 매우 커집니다.


.NET이 기본적으로 4 배의 정렬만으로 성공할 수있는 이유는 무엇입니까? 정렬은 32 비트 시스템에서 4 바이트 청크를 사용하여 수행됩니다. 거기에 무슨 문제가 있습니까?
Tigran

x86에서 런타임이 4 바이트로만 정렬되는 이유는 무엇입니까? 관리되지 않는 코드가 관리 코드를 호출 할 때 추가로주의를 기울이면 64 비트로 정렬 될 수 있다고 생각합니다 . 사양에는 약한 정렬 보장 만 있지만 구현은 더 엄격하게 정렬 할 수 있어야합니다. (사양 : "기본 int에 대한 원자 적 액세스를 위해 기본 하드웨어에 필요한 동일한 경계에 저장 될 때 8 바이트 데이터가 올바르게 정렬됩니다.")
CodesInChaos

1
@Code-C 코드 생성기는 함수 프롤로그의 스택 포인터에서 수학을 수행하여이를 수행 할 수 있습니다. x86 지터는 그렇지 않습니다. 스택에 배열을 할당하는 것이 훨씬 더 일반적이고 8 개로 정렬되는 힙 할당자가 있으므로 스택 할당을 힙 할당보다 덜 효율적으로 만들고 싶지 않기 때문에 네이티브 언어 에서는 훨씬 더 중요합니다. 우리는 32 비트 gc 힙에서 4 개의 정렬이 붙어 있습니다.
Hans Passant

5

결과를 복제하는 데 어려움이 있습니다.

나는 당신의 코드를 가져 왔습니다.

  • 독립형 콘솔 응용 프로그램으로 만들었습니다.
  • 최적화 된 (릴리스) 빌드 빌드
  • "크기"요소를 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);
    }
  }
}

나도 똑같이 했어, 내 결과는 너와 더 비슷해. 플랫폼 및 CPu 유형을 명시하십시오.
Henk Holterman 2011 년

매우 흥미로운! 다른 사람들이 내 결과를 확인하도록했습니다 ... 당신이 가장 먼저 달라졌습니다. 첫 번째 질문 : 내 게시물에서 언급 한 파일의 버전 번호는 무엇입니까? C : \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... Microsoft 문서에서 언급 한 내용입니다. 가지고있는 JIT Optimizer의 버전입니다. (만약 내 사용자에게 .NET을 업그레이드하여 큰 속도 향상을 보라고 말할 수 있다면 나는 행복한 캠핑을 할 수있을 것입니다.하지만 그렇게 간단하지 않을 것 같습니다.)
Brian Kennedy

저는 Visual Studio에서 ... Windows XP SP3에서 ... VMware 가상 머신에서 ... 2.7GHz Intel Core i7에서 실행했습니다. 하지만 제가 관심을 갖는 절대적인 시간은 아닙니다. 비율입니다 ... 저는이 세 가지 방법이 모두 Corey를 위해했던 것과 비슷하게 수행 될 것으로 기대하지만 저에게는 그렇지 않습니다.
Brian Kennedy

내 프로젝트 속성은 다음과 같습니다. 구성 : 릴리스; 플랫폼 : 활성 (x86); 플랫폼 타겟 : x86
Corey Kosak 2011 년

1
mscorwks 버전을 구해 달라는 요청에 대해 ... 죄송합니다. .NET 2.0에 대해이 작업을 실행하길 원하십니까? 내 테스트는 .NET 4.0에서 이루어졌습니다
Corey Kosak

3

여기서 .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에는 존재하지 않는 것 같습니다.


2
예, Structs에 대한 대답은 "최신 JIT 가져 오기"인 것 같습니다. 그러나 Henk의 대답에 대해 물었을 때 왜 메소드가 운영자보다 훨씬 빠릅니까? 두 방법 모두 정확히 동일한 작업을 수행하는 두 연산자보다 5 배 빠릅니다. 구조체를 다시 사용할 수 있다는 것은 대단합니다 ...하지만 여전히 연산자를 피해야한다는 것이 슬프습니다.
브라이언 케네디

Jim, 시스템에있는 C : \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll 파일의 버전을 알고 싶습니다. 내 파일 (.3620)보다 최신 버전 인 경우 Corey (.5446)보다 운영자가 여전히 저처럼 느리지 만 Corey는 그렇지 않은 이유를 설명 할 수 있습니다.
브라이언 케네디

@Brian : 파일 버전 2.0.50727.4214.
짐 미셸

감사! 따라서 사용자가 구조체 최적화를 위해 4214 이상을, 연산자 최적화를 위해 5446 이상을 가지고 있는지 확인해야합니다. 시작할 때 확인하고 경고를 표시하기 위해 코드를 추가해야합니다. 다시 한 번 감사드립니다.
Brian Kennedy

2

@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가 구조체 문제를 수정 한 것으로 보이지만 메서드가 연산자보다 훨씬 빠른 이유에 대한 제 질문이 남아 있습니다. 두 PlusEqual 메서드가 동등한 + = 연산자보다 얼마나 빠른지보십시오. 그리고 그것은 또한 흥미 롭습니다-=가 + =보다 얼마나 빠른지 ... 당신의 타이밍은 내가 그것을 본 첫 번째입니다.
Brian Kennedy

Henk, 시스템에있는 C : \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll 파일의 버전을 알고 싶습니다. Corey (.5446)보다 운영자가 여전히 저처럼 느리지 만 Corey는 그렇지 않은 이유를 설명 할 수 있습니다.
Brian Kennedy

1
.50727 버전 만 찾을 수 있지만 Fx40 / Fx45와 관련이 있는지 확실하지 않습니다.
Henk Holterman 2011 년

나머지 버전 번호를 보려면 속성으로 이동하여 버전 탭을 클릭해야합니다.
Brian Kennedy

2

다른 답변에서 언급 된 JIT 컴파일러 차이점 외에도 struct 메서드 호출과 struct 연산자의 또 다른 차이점은 struct 메서드 호출이 통과한다는 것입니다. thisref 매개 변수 로 되고 다른 매개 변수를 ref매개 변수로 받아들이도록 작성 될 수 있다는 것입니다 . 구조체 연산자는 모든 피연산자를 값으로 전달합니다. 어떤 크기의 구조를 ref매개 변수 로 전달하는 비용은 구조의 크기에 관계없이 고정되어있는 반면, 더 큰 구조를 전달하는 비용은 구조 크기에 비례합니다. 불필요하게 복사하는 것을 피할 수 있다면 큰 구조 (수백 바이트라도)를 사용하는 것은 잘못된 것이 아닙니다 . 방법을 사용하면 불필요한 복사를 방지 할 수 있지만 연산자를 사용하면 방지 할 수 없습니다.


음 ... 글쎄요, 그게 많은 것을 설명 할 수 있습니다! 따라서 연산자가 인라인 될만큼 짧으면 불필요한 복사본을 만들지 않을 것이라고 가정합니다. 그러나 그렇지 않고 구조체가 두 단어 이상이면 속도가 중요한 경우 연산자로 구현하지 않을 수 있습니다. 그 통찰력에 감사드립니다.
Brian Kennedy

BTW, 속도에 대한 질문에 "벤치 마크!"라고 대답 할 때 약간 짜증나는 점이 있습니다. 이러한 응답은 대부분의 경우 작업에 일반적으로 10us 또는 20us가 걸리는지 여부가 중요하지만 약간의 상황 변경으로 인해 1ms 또는 10ms가 걸릴 수 있다는 사실을 무시한다는 것입니다. 중요한 것은 개발자의 컴퓨터에서 무언가가 얼마나 빨리 실행되는지가 아니라 작업이 문제가 될 만큼 충분히 느려질 지 여부입니다 . 방법 X가 대부분의 시스템에서 방법 Y보다 두 배 빠르게 실행되지만 일부 시스템에서는 100 배 느리면 방법 Y가 더 나은 선택 일 수 있습니다.
supercat

물론, 여기서 우리는 단지 2 개의 double에 대해 이야기하고 있습니다. 큰 구조체가 아닙니다. 빠르게 액세스 할 수있는 스택에서 두 개의 double을 전달하는 것은 스택에서 'this'를 전달한 다음이를 끌어 와서 작동시키기 위해 역 참조해야하는 것보다 반드시 느리지는 않습니다.하지만 차이가 발생할 수 있습니다. 그러나이 경우에는 인라인되어야하므로 JIT Optimizer는 정확히 동일한 코드로 끝나야합니다.
Brian Kennedy

1

이것이 관련성이 있는지 확실하지 않지만 다음은 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%.

2
흥미롭게도 32b JIT Optimizer에 추가 된 최적화가 아직 64b JIT Optimizer에 추가되지 않은 것 같습니다. 귀하의 비율은 여전히 ​​저와 매우 유사합니다. 실망 스럽지만 ... 알아두면 좋습니다.
Brian Kennedy

0

구조체의 멤버에 액세스 할 때 실제로 멤버에 액세스하는 추가 작업 인 THIS 포인터 + 오프셋을 수행하는 것처럼 상상할 수 있습니다.


1
글쎄, 클래스 객체를 사용하면 절대적으로 맞을 것입니다. 왜냐하면 메소드가 'this'포인터를 전달하기 때문입니다. 그러나 구조체를 사용하면 그렇게해서는 안됩니다. 구조체는 스택의 메서드로 전달되어야합니다. 따라서 첫 번째 double은 'this'포인터가있는 위치에 있어야하고 두 번째 double은 바로 뒤의 위치에 있어야합니다. 둘 다 CPU에 등록 될 수 있습니다. 따라서 JIT는 기껏해야 오프셋을 사용해야합니다.
Brian Kennedy

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.