Tại sao các nhà khai thác chậm hơn nhiều so với các cuộc gọi phương thức? (cấu trúc chỉ chậm hơn trên các JIT cũ hơn)


84

Giới thiệu: Tôi viết mã hiệu suất cao bằng C #. Có, tôi biết C ++ sẽ mang lại cho tôi sự tối ưu hóa tốt hơn, nhưng tôi vẫn chọn sử dụng C #. Tôi không muốn tranh luận về sự lựa chọn đó. Thay vào đó, tôi muốn nghe ý kiến ​​từ những người, giống như tôi, đang cố gắng viết mã hiệu suất cao trên .NET Framework.

Câu hỏi:

  • Tại sao nhà điều hành trong mã dưới đây lại chậm hơn so với cuộc gọi phương thức tương đương ??
  • Tại sao phương thức truyền hai phần tử kép trong đoạn mã dưới đây nhanh hơn phương thức tương đương truyền một cấu trúc có hai phần tử nhân đôi bên trong? (A: các JIT cũ tối ưu hóa cấu trúc kém hơn)
  • Có cách nào để Trình biên dịch .NET JIT xử lý các cấu trúc đơn giản một cách hiệu quả như các thành viên của cấu trúc không? (A: tải JIT mới hơn)

Những gì tôi nghĩ tôi biết: Trình biên dịch .NET JIT ban đầu sẽ không nội dòng bất kỳ thứ gì liên quan đến cấu trúc. Các cấu trúc kỳ lạ đã cho chỉ nên được sử dụng khi bạn cần các loại giá trị nhỏ phải được tối ưu hóa như các cấu trúc tích hợp, nhưng đúng. May mắn thay, trong .NET 3.5SP1 và .NET 2.0SP2, họ đã thực hiện một số cải tiến đối với Trình tối ưu hóa JIT, bao gồm các cải tiến đối với nội tuyến, đặc biệt là đối với cấu trúc. (Tôi đoán họ đã làm điều đó vì nếu không thì cấu trúc Complex mới mà họ đang giới thiệu sẽ hoạt động khủng khiếp ... vì vậy nhóm Complex có thể đang tấn công nhóm JIT Optimizer.) Vì vậy, bất kỳ tài liệu nào trước .NET 3.5 SP1 có thể là không quá liên quan đến vấn đề này.

Kết quả thử nghiệm của tôi cho thấy: Tôi đã xác minh rằng tôi có Trình tối ưu hóa JIT mới hơn bằng cách kiểm tra xem tệp C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll có phiên bản> = 3053 và do đó sẽ có những cải tiến đó vào trình tối ưu hóa JIT. Tuy nhiên, ngay cả với điều đó, thời gian của tôi và xem xét việc tháo gỡ cả hai đều cho thấy:

Mã do JIT tạo ra để chuyển một cấu trúc có hai phần kép kém hiệu quả hơn nhiều so với mã chuyển trực tiếp hai phần đôi.

Mã do JIT tạo ra cho một phương thức struct chuyển vào 'this' hiệu quả hơn nhiều so với việc bạn chuyển một cấu trúc làm đối số.

JIT vẫn nội tuyến tốt hơn nếu bạn vượt qua hai nhân đôi thay vì truyền một cấu trúc có hai nhân đôi, ngay cả với hệ số do rõ ràng là trong một vòng lặp.

Thời gian: Trên thực tế, nhìn vào phần tháo gỡ, tôi nhận ra rằng hầu hết thời gian trong các vòng lặp chỉ là truy cập dữ liệu thử nghiệm từ Danh sách. Sự khác biệt giữa bốn cách thực hiện cuộc gọi giống nhau là khác nhau đáng kể nếu bạn tính đến mã chi phí của vòng lặp và việc truy cập dữ liệu. Tôi nhận được tốc độ từ 5x đến 20x khi thực hiện PlusEqual (gấp đôi, gấp đôi) thay vì PlusEqual (Yếu tố). Và 10x đến 40x để thực hiện PlusEqual (double, double) thay vì toán tử + =. Chà. Buồn.

Đây là một bộ thời gian:

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%.

Mật mã:

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: (hay còn gọi là những gì ở trên được tổng hợp thành)

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
Chà, điều này nên được tham chiếu như một ví dụ về cách một câu hỏi hay trên Stackoverflow có thể trông như thế nào! Chỉ có thể bỏ qua các nhận xét được tạo tự động. Thật không may, tôi biết quá ít để thực sự đi sâu vào vấn đề, nhưng tôi thực sự thích câu hỏi!
Dennis Traub

2
Tôi không nghĩ Unit Test là một nơi tốt để chạy điểm chuẩn.
Henk Holterman,

1
Tại sao cấu trúc phải nhanh hơn hai lần gấp đôi? Trong .NET, cấu trúc KHÔNG BAO GIỜ bằng nhau, về tổng kích thước của các thành viên của nó. Vì vậy, theo định nghĩa, nó lớn hơn, vì vậy theo định nghĩa, nó phải chậm hơn khi đẩy lên ngăn xếp, sau đó chỉ là 2 giá trị kép. Nếu trình biên dịch sẽ nội tuyến tham số struct trong bộ nhớ kép hàng 2, điều gì sẽ xảy ra nếu bên trong phương thức bạn muốn truy cập cấu trúc đó bằng phản xạ. Thông tin thời gian chạy được liên kết với đối tượng struct đó sẽ ở đâu? Không phải nó, hoặc tôi đang thiếu một cái gì đó?
Tigran

3
@Tigran: Bạn cần nguồn cho những tuyên bố đó. Tôi nghĩ bạn đã nhầm. Chỉ khi một loại giá trị được đóng hộp, siêu dữ liệu mới cần được lưu trữ cùng với giá trị đó. Trong một biến có kiểu cấu trúc tĩnh, không có chi phí.
Ben Voigt

1
Tôi đã nghĩ rằng thứ duy nhất còn thiếu là sự lắp ráp. Và bây giờ bạn đã thêm nó (xin lưu ý, đó là trình hợp dịch x86 chứ KHÔNG phải MSIL).
Ben Voigt

Câu trả lời:


9

Tôi đang nhận được những kết quả rất khác, ít kịch tính hơn nhiều. Nhưng không sử dụng trình chạy thử nghiệm, tôi đã dán mã vào một ứng dụng chế độ điều khiển. Kết quả 5% là ~ 87% ở chế độ 32 bit, ~ 100% ở chế độ 64 bit khi tôi thử.

Căn chỉnh là rất quan trọng trên gấp đôi, thời gian chạy .NET chỉ có thể hứa hẹn căn chỉnh 4 trên máy 32 bit. Có vẻ như người chạy thử nghiệm đang bắt đầu các phương pháp thử nghiệm với địa chỉ ngăn xếp được căn chỉnh thành 4 thay vì 8. Hình phạt căn chỉnh sai sẽ rất lớn khi đường đôi vượt qua ranh giới dòng bộ nhớ cache.


Tại sao .NET về cơ bản có thể thành công khi căn chỉnh chỉ 4 đôi? Việc căn chỉnh được thực hiện bằng cách sử dụng các khối 4 byte trên máy 32 bit. Có vấn đề gì ở đó?
Tigran

Tại sao thời gian chạy chỉ căn chỉnh thành 4 byte trên x86? Tôi nghĩ rằng nó có thể sắp xếp thành 64 bit nếu nó chăm sóc thêm khi mã không được quản lý gọi mã được quản lý. Mặc dù thông số kỹ thuật chỉ có sự đảm bảo liên kết yếu, nhưng việc triển khai sẽ có thể căn chỉnh chặt chẽ hơn. (Spec: "8-byte dữ liệu được sắp xếp đúng khi nó được lưu trữ trên ranh giới cùng theo yêu cầu của phần cứng cơ bản cho việc truy cập nguyên tử đến một int bản địa")
CodesInChaos

1
@Code - Chà, có thể, các trình tạo mã C làm điều này bằng cách thực hiện phép toán trên con trỏ ngăn xếp trong phần mở đầu hàm. Jitter x86 thì không. Đó là nhiều hơn quan trọng cho các ngôn ngữ bản địa kể từ khi bố trí mảng trên stack là phổ biến hơn nhiều và họ có một cấp phát đống rằng Canh lề đến 8 như vậy sẽ không bao giờ muốn làm cho chồng phân bổ kém hiệu quả hơn so với phân bổ heap. Chúng tôi bị mắc kẹt với căn chỉnh 4 từ heap gc 32-bit.
Hans Passant

5

Tôi đang gặp một số khó khăn khi sao chép kết quả của bạn.

Tôi đã lấy mã của bạn:

  • biến nó thành một ứng dụng bảng điều khiển độc lập
  • đã xây dựng một bản dựng (phát hành) được tối ưu hóa
  • đã tăng hệ số "kích thước" từ 2,5 triệu lên 10 triệu
  • chạy nó từ dòng lệnh (bên ngoài IDE)

Khi tôi làm như vậy, tôi nhận được thời gian sau đây khác xa so với thời gian của bạn. Để tránh nghi ngờ, tôi sẽ đăng chính xác mã mà tôi đã sử dụng.

Đây là thời gian của tôi

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%.

Và đây là những chỉnh sửa của tôi đối với mã của bạn:

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);
    }
  }
}

Tôi cũng chỉ làm như vậy, kết quả của tôi giống bạn hơn. Vui lòng nêu nền tảng và loại CPu.
Henk Holterman,

Rất thú vị! Tôi đã có những người khác xác minh kết quả của tôi ... bạn là người đầu tiên nhận ra sự khác biệt. Câu hỏi đầu tiên dành cho bạn: số phiên bản của tệp mà tôi đề cập trong bài đăng của mình là gì ... C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll ... đó là số mà tài liệu của Microsoft cho biết đã chỉ ra phiên bản của Trình tối ưu hóa JIT mà bạn có. (Nếu tôi chỉ có thể nói với người dùng của tôi để nâng cấp .NET của họ để xem speedups lớn, tôi sẽ có một người cắm trại hạnh phúc Nhưng tôi đoán nó không gonna được đơn giản mà..)
Brian Kennedy

Tôi đang chạy bên trong Visual Studio ... chạy trên Windows XP SP3 ... trong máy ảo VMware ... trên Intel Core i7 2.7GHz. Nhưng đó không phải là thời điểm tuyệt đối khiến tôi quan tâm ... đó là tỷ lệ ... Tôi mong đợi ba phương pháp đó đều hoạt động tương tự, điều mà chúng đã làm cho Corey, nhưng KHÔNG đối với tôi.
Brian Kennedy,

Thuộc tính dự án của tôi nói: Cấu hình: Phát hành; Nền tảng: Đang hoạt động (x86); Mục tiêu nền tảng: x86
Corey Kosak

1
Về yêu cầu của bạn để tải phiên bản mscorwks ... Xin lỗi, bạn có muốn tôi chạy thứ này trên .NET 2.0 không? Các bài kiểm tra của tôi trên .NET 4.0
Corey Kosak

3

Đang chạy .NET 4.0 tại đây. Tôi đã biên dịch với "Mọi CPU", nhắm mục tiêu .NET 4.0 ở chế độ phát hành. Thực thi là từ dòng lệnh. Nó chạy ở chế độ 64-bit. Thời gian của tôi hơi khác một chút.

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%.

Đặc biệt, PlusEqual(Element)nhanh hơn một chút so vớiPlusEqual(double, double) .

Dù sự cố xảy ra trong .NET 3.5, nó dường như không tồn tại trong .NET 4.0.


2
Có, câu trả lời trên Structs dường như là "tải JIT mới hơn". Nhưng như tôi đã hỏi về câu trả lời của Henk, tại sao các phương pháp lại nhanh hơn các Operator? Cả hai phương pháp của bạn đều nhanh hơn gấp 5 lần so với một trong hai toán tử của bạn ... đều hoạt động giống hệt nhau. Thật tuyệt là tôi có thể sử dụng lại các cấu trúc ... nhưng đáng buồn là tôi vẫn phải tránh các toán tử.
Brian Kennedy,

Jim, tôi rất muốn biết phiên bản của tệp C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll trên hệ thống của bạn ... nếu mới hơn của tôi (.3620) nhưng cũ hơn hơn Corey's (.5446), thì điều đó có thể giải thích tại sao các toán tử của bạn vẫn chậm như của tôi, nhưng Corey thì không.
Brian Kennedy,

@Brian: Phiên bản tệp 2.0.50727.4214.
Jim Mischel

CẢM ƠN! Vì vậy, tôi cần đảm bảo người dùng của mình có 4214 trở lên để nhận tối ưu hóa cấu trúc và 5446 trở lên để tối ưu hóa toán tử. Tôi cần thêm một số mã để kiểm tra điều đó khi khởi động và đưa ra một số cảnh báo. Cảm ơn một lần nữa.
Brian Kennedy

2

Giống như @Corey Kosak, tôi vừa chạy mã này trong VS 2010 Express dưới dạng một Ứng dụng điều khiển đơn giản ở chế độ Phát hành. Tôi nhận được những con số rất khác nhau. Nhưng tôi cũng có Fx4.5 nên đây có thể không phải là kết quả cho một Fx4.0 sạch.

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%.

Chỉnh sửa: và bây giờ chạy từ dòng cmd. Điều đó tạo ra sự khác biệt và ít biến động hơn trong các con số.


Có, có vẻ như JIT sau đó đã khắc phục sự cố cấu trúc, nhưng câu hỏi của tôi về lý do tại sao các phương thức nhanh hơn nhiều so với các toán tử vẫn còn. Hãy xem cả hai phương thức PlusEqual nhanh hơn bao nhiêu so với toán tử + = tương đương. Và nó cũng thú vị là - = nhanh hơn + = ... thời gian của bạn là lần đầu tiên tôi thấy điều đó.
Brian Kennedy

Henk, tôi rất muốn biết phiên bản của tệp C: \ Windows \ Microsoft.NET \ Framework \ v2.0.50727 \ mscorwks.dll trên hệ thống của bạn ... nếu mới hơn của tôi (.3620), nhưng cũ hơn hơn Corey's (.5446), thì điều đó có thể giải thích tại sao các toán tử của bạn vẫn chậm như của tôi, nhưng Corey thì không.
Brian Kennedy,

1
Tôi chỉ có thể tìm thấy phiên bản .50727 nhưng tôi không chắc liệu phiên bản đó có liên quan đến Fx40 / Fx45 hay không?
Henk Holterman,

Bạn phải vào Thuộc tính và nhấp vào tab Phiên bản để xem phần còn lại của số phiên bản.
Brian Kennedy,

2

Ngoài sự khác biệt của trình biên dịch JIT được đề cập trong các câu trả lời khác, một sự khác biệt khác giữa lời gọi phương thức struct và toán tử struct là lời gọi phương thức struct sẽ truyền thisdưới dạng reftham số (và có thể được viết để chấp nhận các tham số khác cũng như reftham số), trong khi toán tử struct sẽ chuyển tất cả các toán hạng theo giá trị. Chi phí để vượt qua một cấu trúc có kích thước bất kỳ dưới dạng reftham số là cố định, bất kể cấu trúc lớn đến đâu, trong khi chi phí để vượt qua cấu trúc lớn hơn tỷ lệ thuận với kích thước cấu trúc. Không có gì sai khi sử dụng các cấu trúc lớn (thậm chí hàng trăm byte) nếu người ta có thể tránh sao chép chúng một cách không cần thiết ; trong khi các bản sao không cần thiết thường có thể được ngăn chặn khi sử dụng các phương pháp, chúng không thể được ngăn chặn khi sử dụng các toán tử.


Hmmm ... tốt, điều đó có thể giải thích rất nhiều! Vì vậy, nếu toán tử đủ ngắn để nó sẽ được nội dòng, tôi cho rằng nó sẽ không tạo ra các bản sao không cần thiết. Nhưng nếu không, và cấu trúc của bạn có nhiều hơn một từ, bạn có thể không muốn triển khai nó như một toán tử nếu tốc độ là quan trọng. Cảm ơn cho cái nhìn sâu sắc đó.
Brian Kennedy

BTW, một điều làm tôi hơi khó chịu khi các câu hỏi về tốc độ được trả lời "benchmark it!" là phản hồi như vậy bỏ qua thực tế rằng trong nhiều trường hợp, điều quan trọng là liệu một hoạt động thường mất 10us hay 20us, nhưng liệu một sự thay đổi nhỏ của hoàn cảnh có thể khiến nó mất 1ms hay 10ms hay không. Điều quan trọng không phải là thứ gì đó chạy nhanh như thế nào trên máy của nhà phát triển, mà là liệu hoạt động có đủ chậm hay không mới là vấn đề quan trọng ; nếu phương pháp X chạy nhanh gấp đôi phương pháp Y trên hầu hết các máy, nhưng trên một số máy sẽ chậm hơn 100 lần, phương pháp Y có thể là lựa chọn tốt hơn.
supercat

Tất nhiên, ở đây chúng ta đang nói về 2 cấu trúc gấp đôi ... không lớn. Chuyển hai thẻ nhân đôi trên ngăn xếp nơi chúng có thể được truy cập nhanh chóng không nhất thiết phải chậm hơn truyền 'cái này' trên ngăn xếp và sau đó phải tham khảo điều đó để kéo chúng vào hoạt động trên chúng .. nhưng nó có thể gây ra sự khác biệt. Tuy nhiên, trong trường hợp này, nó phải được nội tuyến, do đó, Trình tối ưu hóa JIT phải có cùng một mã.
Brian Kennedy

1

Không chắc liệu điều này có liên quan hay không, nhưng đây là con số cho .NET 4.0 64-bit trên Windows 7 64-bit. Phiên bản mscorwks.dll của tôi là 2.0.50727.5446. Tôi vừa dán mã vào LINQPad và chạy nó từ đó. Đây là kết quả:

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
Thật thú vị ... có vẻ như các tối ưu hóa đã được thêm vào Trình tối ưu hóa JIT 32b vẫn chưa được đưa vào Trình tối ưu hóa JIT 64b ... tỷ lệ của bạn vẫn rất giống với của tôi. Đáng thất vọng ... nhưng tốt biết.
Brian Kennedy

0

Tôi sẽ tưởng tượng như khi bạn đang truy cập các thành viên của cấu trúc, việc thực hiện thêm một thao tác để truy cập thành viên, con trỏ NÀY + bù đắp là không chính xác.


1
Chà, với một đối tượng lớp, bạn hoàn toàn đúng ... bởi vì phương thức sẽ chỉ được chuyển qua con trỏ 'this'. Tuy nhiên, với cấu trúc, điều đó không nên như vậy. Cấu trúc phải được chuyển vào các phương thức trên ngăn xếp. Vì vậy, đôi đầu tiên nên đặt ở vị trí con trỏ 'this' sẽ ở vị trí và đôi thứ hai ở vị trí ngay sau nó ... cả hai đều có thể là thanh ghi trong CPU. Vì vậy, JIT chỉ nên sử dụng tối đa một độ lệch.
Brian Kennedy

0

Có thể thay vì Danh sách, bạn nên sử dụng double [] với hiệu số và gia số chỉ mục "nổi tiếng"?

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.