Benchmarking small code samples in C#, can this implementation be improved?

Dość często NA więc znajduję się benchmarking małe kawałki kodu, aby zobaczyć, która implementacja jest najszybsza.

Dość często widzę komentarze, że kod benchmarking nie uwzględnia jittingu ani garbage collector.

Mam następującą prostą funkcję benchmarkingu, którą powoli ewoluowałem:

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

Użycie:

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

Czy ta implementacja ma jakieś wady? Czy wystarczy pokazać, że implementacja X jest szybsza niż implementacja Y nad Z iteracji? Czy możesz wymyślić jakiś sposób, aby to poprawić?

Edytuj Jest całkiem jasne, że podejście oparte na czasie (w przeciwieństwie do iteracji), jest preferowane, czy ktoś ma jakieś implementacje, w których kontrole czasu nie wpływają na wydajność?

Author: Artemix, 2009-06-26

11 answers

Tutaj jest zmodyfikowana funkcja: zgodnie z zaleceniami społeczności, prosimy o zmianę tej wiki społeczności.

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

Upewnij się, że kompilujesz w wersji z włączoną optymalizacją i uruchamiasz testy poza Visual Studio . Ta ostatnia część jest ważna, ponieważ JIT stints swoje optymalizacje z dołączonym debugerem, nawet w trybie Release.

 89
Author: Sam Saffron,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-03 23:27:15

Finalizacja nie musi być zakończona przed powrotem GC.Collect. Finalizacja jest kolejkowana, a następnie uruchamiana w osobnym wątku. Ten wątek może być nadal aktywny podczas testów, wpływając na wyniki.

Jeśli chcesz upewnić się, że zakończenie zostało zakończone przed rozpoczęciem testów, możesz zadzwonić GC.WaitForPendingFinalizers, która będzie blokowana do czasu wyczyszczenia kolejki finalizacji:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
 22
Author: LukeH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-26 09:50:22

Jeśli chcesz usunąć interakcje GC z równania, możesz uruchomić wywołanie "rozgrzewki" po GC.Nie wcześniej. W ten sposób wiesz, że. NET będzie już mieć wystarczająco dużo pamięci przydzielonej z systemu operacyjnego dla zestawu roboczego funkcji.

Pamiętaj, że dla każdej iteracji wykonujesz wywołanie metody nieinlinowanej, więc upewnij się, że porównujesz rzeczy, które testujesz do pustego ciała. Będziesz musiał również zaakceptować, że można tylko niezawodnie czas rzeczy, które są kilkakrotnie dłuższe niż wywołanie metody.

Ponadto, w zależności od tego, jakiego rodzaju rzeczy profilujesz, możesz chcieć biegać w oparciu o czas przez pewien czas, a nie przez określoną liczbę iteracji-może to prowadzić do łatwiejszych porównywalnych liczb bez konieczności bardzo krótkiego biegu dla najlepszej implementacji i/lub bardzo długiego dla najgorszej.

 15
Author: Jonathan Rupp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-26 04:18:52

W ogóle bym nie zdała delegata:

  1. wywołanie delegata to ~ virtual method call. Nie tani: ~ 25% najmniejszej alokacji pamięci w .NET. jeśli interesują Cię szczegóły, zobacz np. ten link .
  2. anonimowi delegaci mogą prowadzić do użycia zamknięć, których nawet nie zauważysz. Ponownie, dostęp do pól zamknięcia jest zauważalny niż np. dostęp do zmiennej na stosie.

Przykładowy kod prowadzący do zamknięcia:

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

Jeśli nie wiesz o zamknięciach, spójrz na tę metodę w. NET Reflector.

 6
Author: Alex Yakunin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-26 04:19:31

Myślę, że najtrudniejszym problemem do przezwyciężenia z takich metod benchmarkingu jest rozliczanie przypadków krawędzi i nieoczekiwane. Na przykład - " jak działają dwa urywki kodu przy dużym obciążeniu procesora / wykorzystaniu sieci/dyskietce / itp."Są świetne do podstawowych testów logicznych, aby sprawdzić, czy dany algorytm działa znacznie szybciej niż inny. Ale aby poprawnie przetestować większość wydajności kodu, trzeba by stworzyć test, który mierzy konkretne wąskie gardła tego konkretnego kodu.

Nadal powiedziałbym, że testowanie małych bloków kodu często ma niewielki zwrot z inwestycji i może zachęcić do używania zbyt skomplikowanego kodu zamiast prostego kodu do konserwacji. Pisanie jasnego kodu, który inni programiści, lub ja 6 miesięcy w dół linii, mogą szybko zrozumieć, będzie miało więcej korzyści wydajności niż wysoce zoptymalizowany kod.

 6
Author: Paul Alexander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-26 04:22:36

Dzwoniłbym kilka razy na rozgrzewkę, nie tylko jedną.

 5
Author: Alexey Romanov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-07-22 05:15:15

Sugestie dotyczące poprawy

  1. Wykrywanie, czy środowisko wykonawcze jest dobre dla benchmarkingu (np. wykrywanie, czy dołączony jest debugger lub czy optymalizacja jit jest wyłączona, co skutkowałoby nieprawidłowymi pomiarami).

  2. Pomiar części kodu niezależnie (aby dokładnie zobaczyć, gdzie znajduje się wąskie gardło).

  3. porównywanie różnych wersji/komponentów / fragmentów kodu (w pierwszym zdaniu mówisz "... benchmarking małych fragmentów kodu do zobacz, która implementacja jest najszybsza.').

Dotyczące #1:

  • Aby wykryć, czy debugger jest dołączony, przeczytaj Właściwość System.Diagnostics.Debugger.IsAttached (pamiętaj, aby również obsłużyć przypadek, w którym debugger początkowo nie jest dołączony, ale jest dołączony po pewnym czasie).

  • Aby wykryć, czy optymalizacja jit jest wyłączona, przeczytaj właściwość DebuggableAttribute.IsJITOptimizerDisabled odpowiednich zestawów:

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

Dotyczące #2:

Można to zrobić na wiele sposobów. One way jest umożliwienie dostarczenia kilku delegatów, a następnie zmierzenie tych delegatów indywidualnie.

Dotyczące #3:

Można to również zrobić na wiele sposobów, a różne przypadki użycia wymagałyby bardzo różnych rozwiązań. Jeśli test porównawczy jest wywoływany ręcznie, zapis na konsoli może być w porządku. Jeśli jednak benchmark jest wykonywany automatycznie przez system budowania, to pisanie do konsoli prawdopodobnie nie jest takie dobre.

Jednym ze sposobów, aby to zrobić, jest zwrócenie wynik testu porównawczego jako silnie wpisany obiekt, który można łatwo wykorzystać w różnych kontekstach.


Etimo.Benchmarki

Innym podejściem jest wykorzystanie istniejącego komponentu do wykonania wskaźników. Właściwie, w mojej firmie zdecydowaliśmy się udostępnić nasze narzędzie do testów do domeny publicznej. W swoim rdzeniu zarządza garbage collector, jitter, warmups itp., tak jak sugerują niektóre z innych odpowiedzi tutaj. Ma również trzy cechy, które zaproponowałem powyżej. Zarządza kilkoma z tematy poruszane w Eric Lippert blog .

Jest to Przykładowe wyjście, w którym porównywane są dwa komponenty, a wyniki zapisywane są do konsoli. W tym przypadku porównywane dwa komponenty nazywane są 'KeyedCollection' i 'MultiplyIndexedKeyedCollection':

Etimo.Benchmarki-Przykładowe Wyjście Konsoli

Istnieje pakiet NuGet , przykładowy pakiet NuGet, a kod źródłowy jest dostępny na GitHub. Istnieje również blog post .

Jeśli jesteś w pospiesz się, sugeruję, aby pobrać pakiet próbek i po prostu zmodyfikować delegatów próbki w razie potrzeby. Jeśli się nie spieszysz, dobrym pomysłem może być przeczytanie posta na blogu, aby zrozumieć szczegóły.
 4
Author: Joakim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-16 18:03:56

Musisz również uruchomić" rozgrzewkę " przed rzeczywistym pomiarem, aby wykluczyć czas, jaki kompilator JIT spędza na jitowaniu kodu.

 1
Author: Alex Yakunin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-06-26 04:22:32

W zależności od kodu, na którym bazujesz i platformy, na której działa, może być konieczne uwzględnienie tego, jak wyrównanie kodu wpływa na wydajność . Aby to zrobić, prawdopodobnie wymaga zewnętrznego opakowania, które uruchamiało test wiele razy (w oddzielnych domenach aplikacji lub procesach?), czasami po raz pierwszy wywołując "padding code", aby wymusić jego kompilację JIT, tak aby Kod benchmarkingowy był różnie wyrównywany. Pełny wynik testu dałby najlepsze i najgorsze czasy dla różnych wyrównań kodu.

 1
Author: Edward Brey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:31

Jeśli próbujesz wyeliminować wpływ Garbage Collection z benchmark complete, czy warto ustawić GCSettings.LatencyMode?

Jeśli nie, a chcesz, aby wpływ śmieci utworzonych w func był częścią benchmarka, to czy nie powinieneś również wymusić zbierania na końcu testu (wewnątrz timera)?

 1
Author: Danny Tuppeny,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-15 15:35:51

Podstawowym problemem twojego pytania jest założenie, że pojedynczy pomiar może odpowiedzieć na wszystkie pytania. Musisz zmierzyć wielokrotnie, aby uzyskać efektywny obraz sytuacji i szczególnie w śmieciach zebranych jak C#.

Inna odpowiedź daje dobry sposób pomiaru podstawowej wydajności.

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

Jednak ten pojedynczy pomiar nie uwzględnia śmieci kolekcja. Odpowiedni profil dodatkowo stanowi najgorszy przypadek osiągi od śmieci rozrzuconych po wielu połączeniach (numer ten jest sortowany bezużyteczne, ponieważ VM może zakończyć się bez zbierania resztek śmieci, ale nadal jest przydatna do porównywania dwóch różnych implementacje 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);
}

I można też chcieć zmierzyć najgorszy przypadek wydajności garbage collection dla metody, która jest wywoływana tylko raz.

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

Ale ważniejsze niż zalecanie jakichkolwiek konkretnych możliwych dodatkowych pomiary do profilu to pomysł że należy mierzyć wiele różne statystyki, a nie tylko jeden rodzaj statystyki.

 0
Author: Steven Stewart-Gallus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-19 22:08:21