연산자가 메서드 호출보다 훨씬 느린 이유는 무엇입니까? (구조는 이전 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.