2012-03-04 34 views
15

Gần đây tôi đã triển khai thuật toán QuickSort trong C#. Sắp xếp trên một mảng nguyên có chứa hàng triệu mục, hiệu suất của mã này xấp xỉ 10% phía sau việc triển khai .NET.Chi phí của các phương thức nội tuyến trong C#

private static void QS(int[] arr, int left, int right) 
{ 
    if (left >= right) return; 

    var pIndex = Partition(arr, left, right); 
    QS(arr, left, pIndex); 
    QS(arr, pIndex + 1, right); 
} 

Trên một mảng gồm 5 triệu mục, mã này chậm hơn khoảng 60ms so với .NET.

Sau đó, tôi đã tạo phương thức khác có phương pháp Partition() được gạch chân thành QS() (loại bỏ lệnh gọi phương thức và câu lệnh return). Tuy nhiên điều này dẫn đến sự sụt giảm về hiệu suất xuống khoảng 250ms chậm hơn phương pháp sắp xếp của .NET.

Tại sao điều này lại xảy ra?

Chỉnh sửa: Đây là mã theo phương pháp Partition(). Trong phiên bản nội tuyến của QS(), toàn bộ nội dung của phương pháp này ngoại trừ câu lệnh return thay thế dòng var pIndex = Partition(arr, left, right);.

private static int Partition(int[] arr, int left, int right) 
{ 
    int pivot = arr[left]; 
    int leftPoint = left - 1; 
    int pIndex = right + 1; 
    int temp = 0; 

    while (true) 
    { 
     do { pIndex--; } while (arr[pIndex] > pivot); 
     do { leftPoint++; } while (arr[leftPoint] < pivot); 

     if (leftPoint < pIndex) 
     { 
      temp = arr[leftPoint]; 
      arr[leftPoint] = arr[pIndex]; 
      arr[pIndex] = temp; 
     } 
     else { break; } 
    } 
    return pIndex; 
} 

Chỉnh sửa # 2: Trong trường hợp có ai quan tâm trong việc biên soạn, đây là đoạn code mà gọi là thuật toán:

Sửa # 3: mã kiểm tra mới từ gợi ý Haymo của.

private static void Main(string[] args) 
{ 
    const int globalRuns = 10; 
    const int localRuns = 1000; 

    var source = Enumerable.Range(1, 200000).OrderBy(n => Guid.NewGuid()).ToArray(); 
    var a = new int[source.Length]; 

    int start, end, total; 

    for (int z = 0; z < globalRuns; z++) 
    { 
     Console.WriteLine("Run #{0}", z+1); 

     total = 0; 
     for (int i = 0; i < localRuns; i++) 
     { 
      Array.Copy(source, a, source.Length); 
      start = Environment.TickCount; 
      Array.Sort(a); 
      end = Environment.TickCount; 
      total += end - start; 
     } 
     Console.WriteLine("{0}\t\tTtl: {1}ms\tAvg: {2}ms", ".NET", total, total/localRuns); 

     total = 0; 
     for (int i = 0; i < localRuns; i++) 
     { 
      Array.Copy(source, a, source.Length); 
      start = Environment.TickCount; 
      Quicksort.SortInline(a); 
      end = Environment.TickCount; 
      total += end - start; 
     } 
     Console.WriteLine("{0}\t\tTtl: {1}ms\tAvg: {2}ms", "Inlined", total, total/localRuns); 

     total = 0; 
     for (int i = 0; i < localRuns; i++) 
     { 
      Array.Copy(source, a, source.Length); 
      start = Environment.TickCount; 
      Quicksort.SortNonInline(a); 
      end = Environment.TickCount; 
      total += end - start; 
     } 
     Console.WriteLine("{0}\tTtl: {1}ms\tAvg: {2}ms\n", "Not inlined", total, total/localRuns); 
    } 
} 
+0

Bạn có thể cung cấp ví dụ tương thích không? – CodesInChaos

+0

Bạn có thể đăng toàn bộ mã bạn sử dụng, bao gồm mã bạn sử dụng để đo thời gian không? – svick

+2

Trình tối ưu hóa chỉ trong thời gian đã có các phương thức nội tuyến. Nó có thể đưa ra quyết định tốt hơn bạn, nó thực sự biết mã máy trông như thế nào và có thể đánh giá liệu nội tuyến có thực sự là một tối ưu hóa hay chỉ dẫn đến mã bloat. Một tối ưu hóa thực sự là làm cho mã thông minh hơn. Tối ưu hóa QS là tài liệu tốt, chỉ cần nhìn vào bài viết Wikipedia cho người mới bắt đầu. –

Trả lời

13

Dựa trên thông tin được đưa ra trong câu hỏi, người ta chỉ có thể đoán và đặt tên một số ý tưởng.

Bạn đã đo chính xác chưa? Hãy ghi nhớ, để có được kết quả hiệu suất đáng tin cậy, ta nên (ít nhất)

  • Run phát hành xây dựng mà không cần gỡ lỗi gắn
  • Lặp lại bài kiểm tra đầy đủ thường xuyên và trung bình các kết quả
  • Hãy hội chợ kiểm tra, I E. cung cấp cho tất cả các đối tượng thử nghiệm cùng một 'cấu hình nguồn cấp dữ liệu'

Để đảm bảo, hiệu suất của (thực hiện) thả thực sự được kết nối với chức năng nội tuyến có thể điều tra mã IL được tạo. Hoặc thậm chí tốt hơn: các hướng dẫn máy được tạo ra bởi trình biên dịch JIT.

Đối với ILNumerics, chúng tôi đã triển khai sắp xếp nhanh tùy chỉnh và thực hiện rất nhiều biện pháp hiệu suất. Thuật toán cuối cùng nhanh hơn vài lần so với phiên bản CLR. Inlining thủ công chỉ là một cải tiến, đó là cần thiết cho hiệu suất tốt hơn.Những người khác là:

  • Không sử dụng đệ quy
  • Sử dụng so sánh không an toàn/trao đổi
  • Sử dụng chèn sắp xếp cho các phân vùng nhỏ
  • Optimzing cho kích thước hạn chế về tạm thời (stack thay thế) mảng

Rất thường xuyên, nguồn cho kết quả hiệu suất lạ được tìm thấy trong cách, bộ nhớ là (mis) được sử dụng bởi một thuật toán. Khác có thể nằm trong dòng lệnh khác nhau mà cuối cùng nhiều hơn hoặc ít thành công trong việc tối ưu hóa bởi bất kỳ trình biên dịch/bộ vi xử lý có liên quan. Hiệu suất thực hiện tổng thể là một con thú rất phức tạp, khó đoán một cách xác định và do đó một profiler là người bạn tốt nhất của bạn!

@Edit: Bằng cách xem xét thường xuyên kiểm tra chính của bạn, bạn chủ yếu đo băng thông bộ nhớ của bộ xử lý/bộ nhớ chính. Một mảng int [] có chiều dài 5 * 10e6 có kích thước xấp xỉ 19 MB. Điều này có lẽ nằm ngoài phạm vi lưu trữ của bạn. Vì vậy, bộ vi xử lý sẽ chờ bộ nhớ do bộ nhớ cache bắt buộc nhớ hầu hết thời gian. Điều này làm cho một thước đo về ảnh hưởng của bất kỳ cải cách mã nào khó đoán. Tôi đề nghị để cố gắng đo lường theo cách này thay vì: (mã giả)

  • tạo ra dữ liệu thử nghiệm
  • phân bổ mảng cho bản
  • lặp trên số lần lặp lại toàn cầu (hãy nói 10)

    • lặp lại nội bộ cho Array.Sort (ví dụ: 1000)
      • sao chép dữ liệu kiểm tra (chưa phân loại)
      • loại các sao chép bởi Array.Sort
      • thêm thời gian
    • thời gian trung bình cho Array.Sort

    • lặp lại bên trong cho Quicksort.Sort (ví dụ. 1000)

      • copy (phân) dữ liệu thử nghiệm
      • loại các bản sao bởi Quicksort.Sort
      • thêm thời gian
    • thời gian trung bình cho Quicksort.Sort

    • lặp lại bên trong cho Quicksort.Sort2 (ví dụ: 1000)

      • sao chép dữ liệu thử nghiệm (chưa phân loại)
      • sắp xếp sao chép bằng Quicksort.Sort2
      • thêm thời gian
    • thời gian trung bình cho Quicksort.Sort2

Mục đích là để làm cho quicksort chỉ sử dụng dữ liệu từ cache. Do đó, hãy đảm bảo không tạo lại các bản sao từ bộ nhớ mới mà chỉ có hai trường hợp chung: bản gốc và bản sao để sắp xếp. Cả hai đều phải phù hợp với bộ nhớ cache (cấp cuối cùng) của bạn cùng một lúc! Với một số khoảng không (đối với các quy trình khác trên hệ thống), một dự đoán tốt là chỉ sử dụng một nửa kích thước bộ nhớ cache cấp cuối cùng cho cả hai mảng. Tùy thuộc vào kích thước bộ nhớ cache thực sự của bạn, độ dài kiểm tra 250k có vẻ hợp lý hơn.

@ Edit2: Tôi đã chạy mã của bạn, có cùng kết quả và xem hướng dẫn máy (tối ưu hóa) trong trình gỡ lỗi VS. Ở đây có phần có liên quan từ cả hai phiên bản:

Not inlined: 
    69:     do { pIndex--; } while (arr[pIndex] > pivot); 
00000017 dec   ebx 
00000018 cmp   ebx,esi 
0000001a jae   00000053 
0000001c cmp   dword ptr [ecx+ebx*4+8],edi 
00000020 jg   00000017 
    70:     do { leftPoint++; } while (arr[leftPoint] < pivot); 
00000022 inc   edx 
00000023 cmp   edx,esi 
00000025 jae   00000053 
00000027 cmp   dword ptr [ecx+edx*4+8],edi 
0000002b jl   00000022 


Inlined: 
    97:     do { pIndex--; } while (arr[pIndex] > pivot); 
00000038 dec   dword ptr [ebp-14h] 
0000003b mov   eax,dword ptr [ebp-14h] 
0000003e cmp   eax,edi 
00000040 jae   00000097 
00000042 cmp   dword ptr [esi+eax*4+8],ebx 
00000046 jg   00000038 
    98:     do { leftPoint++; } while (arr[leftPoint] < pivot); 
00000048 inc   ecx 
00000049 cmp   ecx,edi 
0000004b jae   00000097 
0000004d cmp   dword ptr [esi+ecx*4+8],ebx 
00000051 jl   00000048 

Như có thể thấy, phiên bản 'Không inlined' không tận dụng tốt hơn các thanh ghi cho decrementing các chỉ số hoạt động (Line 69/97). Rõ ràng là JIT quyết định không đẩy và bật thanh ghi tương ứng trên ngăn xếp, bởi vì các mã khác trong cùng một chức năng đang sử dụng cùng một thanh ghi. Vì đây là một vòng lặp nóng (và CLR không nhận ra điều này) tốc độ thực thi tổng thể bị ảnh hưởng. Vì vậy, trong trường hợp cụ thể này, việc sắp xếp theo cách thủ công hàm Phân vùng sẽ không sinh lợi.

Tuy nhiên, như bạn đã biết, không có gì đảm bảo rằng các phiên bản CLR khác cũng làm như vậy. Sự khác biệt thậm chí có thể phát sinh trong 64 bit.

+0

Cảm ơn, tôi sẽ tiếp tục chơi với thuật toán. Tôi đã cố gắng để có được kết quả đáng tin cậy mặc dù bằng cách làm tất cả ba đề xuất của bạn. Tôi đã đính kèm mã chương trình chính nếu bạn muốn biên dịch nó. – Raintree

+0

+1 bạn đã (lại) nhanh hơn ... argh;) –

+0

@PaulWendler Xin lỗi –

Các vấn đề liên quan