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

소개 : 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 메서드 호출이 통과한다는 것입니다. thisref 매개 변수 로 되고 다른 매개 변수를 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 포인터 + 오프셋을 수행하는 것처럼 상상할 수 있습니다.