Đo điểm chuẩn các mẫu mã nhỏ trong C #, việc triển khai này có thể được cải thiện không?


104

Khá thường xuyên trên SO, tôi thấy mình tự đánh giá điểm chuẩn của các đoạn mã nhỏ để xem việc nhập nào là nhanh nhất.

Khá thường xuyên tôi thấy các nhận xét rằng mã điểm chuẩn không tính đến việc lắp ráp hoặc trình thu gom rác.

Tôi có chức năng đo điểm chuẩn đơn giản sau đây mà tôi đã phát triển từ từ:

  static void Profile(string description, int iterations, Action func) {
        // warm up 
        func();
        // clean up
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++) {
            func();
        }
        watch.Stop();
        Console.Write(description);
        Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
    }

Sử dụng:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});

Việc triển khai này có bất kỳ sai sót nào không? Nó có đủ tốt để chỉ ra rằng implementaion X nhanh hơn các lần lặp thực hiện Y qua Z không? Bạn có thể nghĩ ra cách nào để cải thiện điều này không?

CHỈNH SỬA Khá rõ ràng rằng phương pháp tiếp cận dựa trên thời gian (trái ngược với lặp lại), được ưa thích hơn, có ai có bất kỳ triển khai nào mà việc kiểm tra thời gian không ảnh hưởng đến hiệu suất không?


Câu trả lời:


95

Đây là chức năng đã được sửa đổi: theo khuyến nghị của cộng đồng, vui lòng sửa đổi chức năng này là wiki cộng đồng.

static double Profile(string description, int iterations, Action func) {
    //Run at highest priority to minimize fluctuations caused by other processes/threads
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
    Thread.CurrentThread.Priority = ThreadPriority.Highest;

    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
    return watch.Elapsed.TotalMilliseconds;
}

Đảm bảo rằng bạn biên dịch trong Bản phát hành có bật tính năng tối ưu hóa và chạy các thử nghiệm bên ngoài Visual Studio . Phần cuối cùng này rất quan trọng vì JIT tiến hành tối ưu hóa bằng trình gỡ lỗi được đính kèm, ngay cả trong chế độ Phát hành.


Bạn có thể muốn hủy cuộn vòng lặp theo một số lần, chẳng hạn như 10, để giảm thiểu chi phí vòng lặp.
Mike Dunlavey

2
Tôi vừa cập nhật để sử dụng Stopwatch.StartNew. Không phải là một thay đổi chức năng, nhưng lưu một dòng mã.
LukeH

1
@Luke, sự thay đổi tuyệt vời (Tôi ước mình có thể +1 nó). @ Mike im không chắc chắn, tôi nghi ngờ overhead virtualcall sẽ cao hơn nhiều rằng sự so sánh và phân công, do đó hiệu suất diff sẽ không đáng kể
Sam Saffron

Tôi đề xuất bạn chuyển số lần lặp lại cho Hành động và tạo vòng lặp ở đó (có thể - thậm chí là chưa được cuộn). Trong trường hợp bạn đang đo hoạt động tương đối ngắn, đây là lựa chọn duy nhất. Và tôi muốn xem số liệu nghịch đảo - ví dụ: số lần chuyền / giây.
Alex Yakunin

2
Bạn nghĩ gì về việc hiển thị thời gian trung bình. Một cái gì đó như sau: Console.WriteLine ("Thời gian trung bình đã trôi qua {0} mili giây", watch.ElapsedMilliseconds / lần lặp);
rudimenter

22

Quyết toán không nhất thiết phải được hoàn thành trước khi GC.Collecttrả hàng Quá trình hoàn thiện được xếp hàng đợi và sau đó chạy trên một chuỗi riêng biệt. Chuỗi này vẫn có thể hoạt động trong quá trình bạn kiểm tra, ảnh hưởng đến kết quả.

Nếu bạn muốn đảm bảo rằng quá trình hoàn thành đã hoàn tất trước khi bắt đầu các thử nghiệm của mình thì bạn có thể muốn gọi GC.WaitForPendingFinalizers, thao tác này sẽ chặn cho đến khi hàng đợi hoàn thành được xóa:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

10
Tại sao lại GC.Collect()một lần nữa?
colinfang

7
@colinfang Vì các đối tượng đang được "hoàn thiện" không được trình hoàn thiện GC. Vì vậy, thứ hai Collectlà ở đó để đảm bảo các đối tượng "cuối cùng" cũng được thu thập.
MAV

15

Nếu bạn muốn loại bỏ các tương tác GC ra khỏi phương trình, bạn có thể muốn chạy lệnh gọi 'khởi động' sau cuộc gọi GC.Collect, không phải trước đó. Bằng cách đó, bạn biết .NET sẽ có đủ bộ nhớ được cấp phát từ Hệ điều hành cho nhóm hoạt động của chức năng của bạn.

Hãy nhớ rằng bạn đang thực hiện một lệnh gọi phương thức không nội tuyến cho mỗi lần lặp, vì vậy hãy đảm bảo rằng bạn so sánh những thứ bạn đang thử nghiệm với phần thân trống. Bạn cũng sẽ phải chấp nhận rằng bạn chỉ có thể tính thời gian một cách đáng tin cậy cho những thứ lâu hơn một vài lần so với một cuộc gọi phương thức.

Ngoài ra, tùy thuộc vào loại nội dung bạn đang lập hồ sơ, bạn có thể muốn chạy dựa trên thời gian của mình trong một khoảng thời gian nhất định thay vì cho một số lần lặp lại nhất định - nó có thể dẫn đến các con số dễ so sánh hơn mà không phải có một thời gian rất ngắn để thực hiện tốt nhất và / hoặc một thời gian rất dài cho điều tồi tệ nhất.


1
điểm tốt, bạn có lưu ý đến việc triển khai dựa trên thời gian không?
Sam Saffron

6

Tôi không muốn vượt qua đại biểu nào cả:

  1. Cuộc gọi ủy nhiệm là ~ cuộc gọi phương thức ảo. Không rẻ: ~ 25% phân bổ bộ nhớ nhỏ nhất trong .NET. Nếu bạn quan tâm đến chi tiết, hãy xem ví dụ: liên kết này .
  2. Các đại biểu ẩn danh có thể dẫn đến việc sử dụng các lệnh đóng mà bạn thậm chí sẽ không nhận thấy. Một lần nữa, việc truy cập các trường đóng lại đáng chú ý hơn là truy cập một biến trên ngăn xếp.

Một mã ví dụ dẫn đến việc sử dụng đóng cửa:

public void Test()
{
  int someNumber = 1;
  Profiler.Profile("Closure access", 1000000, 
    () => someNumber + someNumber);
}

Nếu bạn không biết về các bao đóng, hãy xem phương pháp này trong .NET Reflector.


Điểm thú vị, nhưng làm thế nào bạn sẽ tạo một phương thức Profile () có thể sử dụng lại nếu bạn không vượt qua một đại biểu? Có những cách nào khác để truyền mã tùy ý cho một phương thức không?
Ash

1
Chúng tôi sử dụng "using (Đo lường mới (...)) {... mã được đo lường ...}". Vì vậy, chúng tôi nhận được đối tượng Đo lường triển khai IDisposable thay vì truyền đại biểu. Xem code.google.com/p/dataobjectsdotnet/source/browse/Xtensive.Core/…
Alex Yakunin

Điều này sẽ không dẫn đến bất kỳ vấn đề nào với việc đóng cửa.
Alex Yakunin

3
@AlexYakunin: liên kết của bạn dường như đã bị hỏng. Bạn có thể đưa mã cho lớp Đo lường trong câu trả lời của mình không? Tôi nghi ngờ rằng bất kể bạn triển khai nó như thế nào, bạn sẽ không thể chạy mã được định dạng nhiều lần với cách tiếp cận IDisposable này. Tuy nhiên, nó thực sự rất hữu ích trong các tình huống mà bạn muốn đo lường các phần khác nhau của một ứng dụng phức tạp (đan xen) đang hoạt động như thế nào, miễn là bạn lưu ý rằng các phép đo có thể không chính xác và không nhất quán khi chạy vào các thời điểm khác nhau. Tôi đang sử dụng cách tiếp cận tương tự trong hầu hết các dự án của mình.
ShdNx

1
Yêu cầu chạy kiểm tra hiệu suất nhiều lần thực sự quan trọng (khởi động + nhiều phép đo), vì vậy tôi cũng đã chuyển sang phương pháp tiếp cận với đại biểu. Hơn nữa, nếu bạn không sử dụng bao đóng, việc gọi đại biểu sẽ nhanh hơn sau đó gọi phương thức giao diện trong trường hợp với IDisposable.
Alex Yakunin,

6

Tôi nghĩ vấn đề khó khắc phục nhất với các phương pháp đo điểm chuẩn như thế này là tính đến các trường hợp cạnh tranh và không mong đợi. Ví dụ - "Làm thế nào để hai đoạn mã hoạt động trong điều kiện tải CPU cao / sử dụng mạng / đập đĩa / v.v." Họ đang tuyệt vời để kiểm tra logic cơ bản để xem nếu một thuật toán đặc biệt các công trình đáng kể nhanh hơn khác. Nhưng để kiểm tra chính xác hầu hết hiệu suất mã, bạn phải tạo một bài kiểm tra đo lường các nút cổ chai cụ thể của mã cụ thể đó.

Tôi vẫn muốn nói rằng thử nghiệm các khối mã nhỏ thường có ít lợi tức đầu tư và có thể khuyến khích sử dụng mã quá phức tạp thay vì mã đơn giản có thể bảo trì. Viết mã rõ ràng mà các nhà phát triển khác, hoặc bản thân tôi sau 6 tháng, có thể hiểu nhanh chóng sẽ có nhiều lợi ích về hiệu suất hơn là mã được tối ưu hóa cao.


1
quan trọng là một trong những điều khoản thực sự được tải. đôi khi việc triển khai nhanh hơn 20% là đáng kể, đôi khi phải nhanh hơn 100 lần mới đáng kể. Đồng ý với bạn trên rõ ràng see: stackoverflow.com/questions/1018407/...
Sam Saffron

Trong trường hợp này, không phải tất cả những thứ quan trọng đều được tải. Bạn đang so sánh một hoặc nhiều triển khai đồng thời và nếu sự khác biệt về hiệu suất của hai triển khai đó không có ý nghĩa thống kê, thì không đáng để thực hiện phương pháp phức tạp hơn.
Paul Alexander

5

Tôi sẽ gọi func()nhiều lần để khởi động, không chỉ một lần.


1
Mục đích là để đảm bảo việc biên dịch jit được thực hiện, bạn nhận được lợi thế nào khi gọi func nhiều lần trước khi đo lường?
Sam Saffron

3
Để JIT có cơ hội cải thiện kết quả đầu tiên của nó.
Alexey Romanov

1
.NET JIT không cải thiện kết quả của nó theo thời gian (giống như Java). Nó chỉ chuyển đổi một phương thức từ IL thành Assembly một lần, trong lần gọi đầu tiên.
Matt Warren

4

Góp ý để phát triển

  1. Phát hiện xem môi trường thực thi có tốt cho việc đo điểm chuẩn hay không (chẳng hạn như phát hiện nếu trình gỡ lỗi được đính kèm hoặc nếu tối ưu hóa jit bị vô hiệu hóa dẫn đến các phép đo không chính xác).

  2. Đo lường các phần của mã một cách độc lập (để xem chính xác nút cổ chai ở đâu).

  3. So sánh các phiên bản / thành phần / đoạn mã khác nhau (Trong câu đầu tiên, bạn nói '... đánh giá điểm chuẩn các đoạn mã nhỏ để xem việc triển khai nào là nhanh nhất.').

Về số 1:

  • Để phát hiện xem trình gỡ lỗi có được đính kèm hay không, hãy đọc thuộc tính System.Diagnostics.Debugger.IsAttached(Hãy nhớ cũng xử lý trường hợp trình gỡ lỗi ban đầu không được đính kèm, nhưng được đính kèm sau một thời gian).

  • Để phát hiện xem tối ưu hóa jit có bị vô hiệu hóa hay không, hãy đọc DebuggableAttribute.IsJITOptimizerDisabledthuộc tính của các cụm liên quan:

    private bool IsJitOptimizerDisabled(Assembly assembly)
    {
        return assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
            .Select(customAttribute => (DebuggableAttribute) customAttribute)
            .Any(attribute => attribute.IsJITOptimizerDisabled);
    }

Về # 2:

Điều này có thể được thực hiện bằng nhiều cách. Một cách là cho phép cung cấp một số đại biểu và sau đó đo lường từng đại biểu đó.

Về # 3:

Điều này cũng có thể được thực hiện theo nhiều cách và các trường hợp sử dụng khác nhau sẽ yêu cầu các giải pháp rất khác nhau. Nếu điểm chuẩn được gọi theo cách thủ công, thì việc ghi vào bảng điều khiển có thể ổn. Tuy nhiên, nếu điểm chuẩn được thực hiện tự động bởi hệ thống xây dựng, thì việc ghi vào bảng điều khiển có lẽ không ổn như vậy.

Một cách để làm điều này là trả về kết quả điểm chuẩn dưới dạng một đối tượng được đánh mạnh có thể dễ dàng sử dụng trong các ngữ cảnh khác nhau.


Etimo.Benchmarks

Một cách tiếp cận khác là sử dụng một thành phần hiện có để thực hiện các điểm chuẩn. Trên thực tế, tại công ty của tôi, chúng tôi đã quyết định phát hành công cụ điểm chuẩn của mình cho phạm vi công cộng. Về cốt lõi, nó quản lý bộ thu gom rác, jitter, warmups, v.v., giống như một số câu trả lời khác ở đây đề xuất. Nó cũng có ba tính năng mà tôi đã đề xuất ở trên. Nó quản lý một số vấn đề được thảo luận trong blog Eric Lippert .

Đây là đầu ra ví dụ trong đó hai thành phần được so sánh và kết quả được ghi vào bảng điều khiển. Trong trường hợp này, hai thành phần được so sánh được gọi là 'KeyedCollection' và 'MultiplyIndexedKeyedCollection':

Etimo.Benchmarks - Đầu ra bảng điều khiển mẫu

Có một gói NuGet , một gói NuGet mẫu và mã nguồn có sẵn tại GitHub . Ngoài ra còn có một bài đăng trên blog .

Nếu bạn đang vội, tôi khuyên bạn nên lấy gói mẫu và chỉ cần sửa đổi các đại biểu mẫu nếu cần. Nếu bạn không vội, bạn nên đọc bài đăng trên blog để hiểu chi tiết.


1

Bạn cũng phải chạy vượt qua "khởi động" trước khi đo lường thực tế để loại trừ thời gian trình biên dịch JIT dành cho việc cài đặt mã của bạn.


nó được thực hiện trước khi đo
Sam Saffron

1

Tùy thuộc vào mã mà bạn đang đo điểm chuẩn và nền tảng mà nó chạy, bạn có thể cần tính đến cách căn chỉnh mã ảnh hưởng đến hiệu suất . Để làm như vậy có thể sẽ yêu cầu một trình bao bọc bên ngoài chạy thử nghiệm nhiều lần (trong các miền hoặc quy trình ứng dụng riêng biệt?), Một số lần đầu tiên gọi "mã đệm" để buộc nó phải được biên dịch JIT, để làm cho mã bị chuẩn để được căn chỉnh khác nhau. Một kết quả thử nghiệm hoàn chỉnh sẽ cung cấp thời gian trong trường hợp tốt nhất và trường hợp xấu nhất cho các căn chỉnh mã khác nhau.


1

Nếu bạn đang cố gắng loại bỏ tác động của Bộ sưu tập rác khỏi điểm chuẩn, thì nó có đáng đặt GCSettings.LatencyModekhông?

Nếu không, và bạn muốn tác động của rác được tạo ra functrở thành một phần của điểm chuẩn, thì bạn cũng không nên bắt buộc thu thập vào cuối bài kiểm tra (bên trong bộ đếm thời gian)?


0

Vấn đề cơ bản với câu hỏi của bạn là giả định rằng một phép đo duy nhất có thể trả lời tất cả các câu hỏi của bạn. Bạn cần phải đo lường nhiều lần để có được một bức tranh hiệu quả về tình hình và đặc biệt là trong một langauge được thu gom rác như C #.

Một câu trả lời khác cung cấp một cách ổn định để đo lường hiệu suất cơ bản.

static void Profile(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

Tuy nhiên, phép đo đơn lẻ này không tính đến việc thu gom rác. Một hồ sơ phù hợp cũng giải thích cho trường hợp xấu nhất của việc thu gom rác được trải ra trong nhiều lệnh gọi (con số này vô dụng vì VM có thể kết thúc mà không bao giờ thu thập rác còn lại nhưng vẫn hữu ích để so sánh hai cách triển khai khác nhau của func.)

static void ProfileGarbageMany(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

Và người ta cũng có thể muốn đo hiệu suất trong trường hợp xấu nhất của việc thu gom rác cho một phương pháp chỉ được gọi một lần.

static void ProfileGarbage(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

Nhưng quan trọng hơn việc đề xuất bất kỳ phép đo bổ sung cụ thể nào có thể được lập hồ sơ là ý tưởng rằng người ta nên đo lường nhiều thống kê khác nhau chứ không chỉ một loại thống kê.

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.