Dość często w SO zdaję sobie sprawę, że porównuję małe fragmenty kodu, aby zobaczyć, która implementacja jest najszybsza.
Dość często widzę komentarze, że kod benchmarkingu nie bierze pod uwagę jittingu ani garbage collectora.
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);
}
Stosowanie:
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 przez iteracje Z? Czy możesz wymyślić jakiś sposób, w jaki mógłbyś to poprawić?
EDYCJA Jest całkiem jasne, że preferowane jest podejście oparte na czasie (w przeciwieństwie do iteracji), czy ktoś ma jakieś implementacje, w których sprawdzanie czasu nie wpływa na wydajność?
c#
.net
performance
profiling
Sam Saffron
źródło
źródło
Odpowiedzi:
Oto zmodyfikowana funkcja: zgodnie z zaleceniami społeczności, nie krępuj się zmienić tego, jest to wiki społeczności.
Upewnij się, że kompilujesz w wersji z włączonymi optymalizacjami i uruchamiasz testy poza programem Visual Studio . Ta ostatnia część jest ważna, ponieważ JIT wykonuje optymalizacje z dołączonym debugerem, nawet w trybie wydania.
źródło
Finalizacja niekoniecznie musi zostać zakończona przed
GC.Collect
zwrotem. Finalizacja jest umieszczana w kolejce, 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 się upewnić, że finalizacja została zakończona przed rozpoczęciem testów, możesz zadzwonić
GC.WaitForPendingFinalizers
, co będzie blokować do czasu wyczyszczenia kolejki finalizacji:źródło
GC.Collect()
jeszcze raz?Collect
aby upewnić się, że „sfinalizowane” obiekty również zostaną zebrane.Jeśli chcesz wykluczyć interakcje GC z równania, możesz zechcieć wywołać rozgrzewkę po wywołaniu GC.Collect, a nie przed. W ten sposób wiesz, że .NET będzie już mieć wystarczającą ilość pamięci przydzielonej z systemu operacyjnego dla zestawu roboczego funkcji.
Pamiętaj, że dla każdej iteracji wykonujesz wywołanie metody niewymienionej, więc upewnij się, że porównujesz rzeczy, które testujesz, z pustą treścią. Musisz także zaakceptować fakt, że możesz niezawodnie mierzyć czas tylko rzeczy, które są kilka razy dłuższe niż wywołanie metody.
Ponadto, w zależności od tego, jakiego rodzaju rzeczy profilujesz, możesz chcieć uruchomić czas w oparciu o określony czas, a nie przez określoną liczbę iteracji - może to prowadzić do łatwiejszych do porównania liczb bez konieczność posiadania bardzo krótkiego okresu dla najlepszej implementacji i / lub bardzo długiego dla najgorszego.
źródło
W ogóle unikałbym minięcia delegata:
Przykładowy kod prowadzący do użycia zamknięcia:
Jeśli nie wiesz o domknięciach, przyjrzyj się tej metodzie w .NET Reflector.
źródło
IDisposable
.Myślę, że najtrudniejszym problemem do przezwyciężenia za pomocą metod analizy porównawczej, takich jak ta, jest uwzględnienie przypadków skrajnych i nieoczekiwanych. Na przykład - „Jak działają dwa fragmenty kodu przy dużym obciążeniu procesora / wykorzystaniu sieci / wyrzucaniu dysków / itp.”. Doskonale nadają się do podstawowych sprawdzeń logicznych, aby sprawdzić, czy określony algorytm działa znacznie szybciej niż inny. Aby jednak poprawnie przetestować wydajność większości kodu, należałoby utworzyć test, który mierzy 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ęcać do używania zbyt złożonego kodu zamiast prostego kodu, który można konserwować. Pisanie przejrzystego kodu, który inni programiści lub ja po sześciu miesiącach możemy szybko zrozumieć, przyniesie więcej korzyści w zakresie wydajności niż wysoce zoptymalizowany kod.
źródło
Wzywałbym
func()
kilka razy na rozgrzewkę, a nie tylko jedną.źródło
Sugestie dotyczące ulepszeń
Wykrywanie, czy środowisko wykonawcze jest dobre do testów porównawczych (na przykład wykrywanie, czy debugger jest dołączony lub czy optymalizacja jit jest wyłączona, co spowodowałoby nieprawidłowe pomiary).
Niezależne pomiary części kodu (aby dokładnie zobaczyć, gdzie znajduje się wąskie gardło).
Odnośnie nr 1:
Aby wykryć, czy debugger jest dołączony, przeczytaj właściwość
System.Diagnostics.Debugger.IsAttached
(pamiętaj, aby obsłużyć również przypadek, w którym debugger nie jest początkowo dołączony, ale jest dołączany po pewnym czasie).Aby wykryć, czy optymalizacja jit jest wyłączona, przeczytaj właściwość
DebuggableAttribute.IsJITOptimizerDisabled
odpowiednich zestawów:Odnośnie nr 2:
Można to zrobić na wiele sposobów. Jednym ze sposobów jest zezwolenie na dostarczenie kilku delegatów, a następnie indywidualny pomiar tych delegatów.
Odnośnie # 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 do konsoli może być w porządku. Jeśli jednak test porównawczy jest wykonywany automatycznie przez system kompilacji, zapisywanie do konsoli prawdopodobnie nie jest takie dobre.
Jednym ze sposobów jest zwrócenie wyniku testu porównawczego jako obiektu o jednoznacznie określonym typie, który można łatwo wykorzystać w różnych kontekstach.
Etimo.Benchmarks
Innym podejściem jest użycie istniejącego komponentu do wykonania testów porównawczych. Właściwie w mojej firmie zdecydowaliśmy się udostępnić nasze narzędzie testowe do domeny publicznej. W swej istocie zarządza kolektorem śmieci, jitterem, rozgrzewkami itp., Tak jak sugerują niektóre inne odpowiedzi. Ma również trzy funkcje, które zasugerowałem powyżej. Zarządza kilkoma zagadnieniami omawianymi na blogu Erica Lipperta .
To jest przykładowy wynik, w którym porównywane są dwa komponenty, a wyniki są zapisywane w konsoli. W tym przypadku dwa porównywane składniki nazywane są „KeyedCollection” i „MultiplyIndexedKeyedCollection”:
Istnieje pakiet NuGet , przykładowy pakiet NuGet, a kod źródłowy jest dostępny w witrynie GitHub . Jest też wpis na blogu .
Jeśli się spieszysz, sugeruję pobranie przykładowego pakietu i po prostu zmodyfikowanie przykładowych delegatów w razie potrzeby. Jeśli się nie spieszysz, dobrym pomysłem może być przeczytanie posta na blogu, aby zrozumieć szczegóły.
źródło
Musisz także przeprowadzić „rozgrzewkę” przed rzeczywistym pomiarem, aby wykluczyć czas, jaki kompilator JIT poświęca na jowanie kodu.
źródło
W zależności od kodu, który jest testowany, i platformy, na której działa, może być konieczne uwzględnienie wpływu wyrównania kodu na wydajność . Aby to zrobić, prawdopodobnie wymagałoby to zewnętrznego opakowania, które uruchamiało test wiele razy (w oddzielnych domenach aplikacji lub procesach?), Czasami najpierw wywołując „kod uzupełniający”, aby wymusić kompilację JIT, aby kod był testowane w celu dostosowania w inny sposób. Pełny wynik testu dałby najlepsze i najgorsze czasy dla różnych dopasowań kodu.
źródło
Jeśli próbujesz wyeliminować wpływ Garbage Collection z testu porównawczego, czy warto to ustawić
GCSettings.LatencyMode
?Jeśli nie, a chcesz, aby wpływ utworzonych śmieci
func
był częścią testu porównawczego, to czy nie powinieneś również wymuszać zbierania danych pod koniec testu (wewnątrz licznika czasu)?źródło
Podstawowym problemem związanym z twoim pytaniem jest założenie, że pojedynczy pomiar może odpowiedzieć na wszystkie twoje pytania. Aby uzyskać skuteczny obraz sytuacji, musisz wykonywać pomiary wiele razy, zwłaszcza w języku zbierania śmieci, takim jak C #.
Inna odpowiedź daje dobry sposób pomiaru podstawowej wydajności.
Jednak ten pojedynczy pomiar nie uwzględnia wyrzucania elementów bezużytecznych. Odpowiedni profil dodatkowo uwzględnia najgorszy przypadek wydajności wyrzucania elementów bezużytecznych rozłożonych na wiele wywołań (ta liczba jest trochę bezużyteczna, ponieważ maszyna wirtualna może się zakończyć bez zbierania pozostałych śmieci, ale nadal jest przydatna do porównywania dwóch różnych implementacji
func
).Można też chcieć zmierzyć wydajność czyszczenia pamięci w najgorszym przypadku dla metody, która jest wywoływana tylko raz.
Ale ważniejsze niż zalecanie jakichkolwiek konkretnych możliwych dodatkowych pomiarów do profilowania jest idea, że należy mierzyć wiele różnych statystyk, a nie tylko jeden rodzaj statystyki.
źródło