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