Jak napisać poprawny mikro-test porównawczy w Javie?

870

Jak piszesz (i uruchamiasz) poprawny mikro-test w Javie?

Szukam przykładów kodu i komentarzy ilustrujących różne rzeczy do przemyślenia.

Przykład: Czy test porównawczy powinien mierzyć czas / iterację lub iteracje / czas i dlaczego?

Powiązane: Czy testowanie stopera jest dopuszczalne?

John Nilsson
źródło
Zobacz [to pytanie] [1] sprzed kilku minut, aby uzyskać powiązane informacje. edycja: przepraszam, to nie powinna być odpowiedź. Powinienem był opublikować jako komentarz. [1]: stackoverflow.com/questions/503877/…
Tiago
Dopiero po tym, jak postanowiłem odnieść plakat tego pytania do takiego pytania, zauważyłem, że to pytanie nie istnieje. Mam nadzieję, że z czasem zgromadzi kilka dobrych wskazówek.
John Nilsson,
5
Java 9 może oferować pewne funkcje do mikro-testów: openjdk.java.net/jeps/230
Raedwald
1
@ Raedwald Myślę, że ten JEP ma na celu dodanie mikro-testu porównawczego do kodu JDK, ale nie sądzę, że jmh zostanie włączony do JDK ...
assylias
1
@ Raedwald Witam z przyszłości. Nie zrobił cięcia .
Michael

Odpowiedzi:

787

Wskazówki dotyczące pisania mikroprocesorów od twórców Java HotSpot :

Zasada 0: Przeczytaj renomowany artykuł na temat maszyn JVM i mikro-benchmarkingu. Dobrym jest Brian Goetz, 2005 . Nie oczekuj zbyt wiele od mikro-testów; mierzą tylko ograniczony zakres charakterystyki wydajności JVM.

Reguła 1: Zawsze dołączaj fazę rozgrzewki, która uruchamia jądro testowe przez cały czas, wystarczającą do uruchomienia wszystkich inicjalizacji i kompilacji przed fazami czasowymi. (Mniej iteracji jest w porządku w fazie rozgrzewania. Zasadą jest kilkadziesiąt tysięcy iteracji w wewnętrznej pętli).

Zasada nr 2: Zawsze uruchamiaj z -XX:+PrintCompilation, -verbose:gcitd, więc można sprawdzić, że kompilator i inne części JVM nie robią nieoczekiwany prace podczas fazy rozrządu.

Reguła 2.1: Drukuj wiadomości na początku i na końcu faz synchronizacji i rozgrzewki, abyś mógł sprawdzić, czy nie ma wyjścia z reguły 2 podczas fazy synchronizacji.

Zasada 3: Bądź świadomy różnicy między -clienti -server, a OSR i regularnymi kompilacjami. -XX:+PrintCompilationFlaga informuje OSR składanki z co-znak oznaczający zakaz początkowy punkt wejścia, np Trouble$1::run @ 2 (41 bytes). Preferuj serwer od klienta, a normalnie OSR, jeśli zależy Ci na najlepszej wydajności.

Zasada 4: Bądź świadomy efektów inicjalizacji. Nie drukuj po raz pierwszy podczas fazy pomiaru czasu, ponieważ drukowanie ładuje i inicjuje klasy. Nie ładuj nowych klas poza fazą rozgrzewki (lub końcową fazą raportowania), chyba że testujesz ładowanie klas konkretnie (iw takim przypadku ładuj tylko klasy testowe). Zasada 2 jest pierwszą linią obrony przed takimi efektami.

Zasada 5: Bądź świadomy efektów deoptimizacji i rekompilacji. Nie bierz żadnej ścieżki kodu po raz pierwszy w fazie synchronizacji, ponieważ kompilator może śmieci i ponownie skompilować kod, w oparciu o wcześniejsze optymistyczne założenie, że ścieżka nie będzie w ogóle używana. Zasada 2 jest pierwszą linią obrony przed takimi efektami.

Reguła 6: Użyj odpowiednich narzędzi, aby odczytać zdanie kompilatora i spodziewaj się zaskoczenia kodem, który produkuje. Sprawdź sam kod przed sformułowaniem teorii na temat tego, co czyni coś szybszym lub wolniejszym.

Zasada 7: Zmniejsz hałas w pomiarach. Uruchom test porównawczy na cichej maszynie i uruchom go kilka razy, odrzucając wartości odstające. Użyj -Xbatchdo serializacji kompilatora z aplikacją i rozważ ustawienie, -XX:CICompilerCount=1aby uniemożliwić równoległe działanie kompilatora. Postaraj się zmniejszyć obciążenie GC, ustaw Xmx(wystarczająco duży) równość Xmsi użyj, UseEpsilonGCjeśli jest dostępny.

Zasada 8: Użyj biblioteki do testu porównawczego, ponieważ jest ona prawdopodobnie bardziej wydajna i została już debugowana wyłącznie w tym celu. Takich jak JMH , Caliper lub Bill and Paul's Excellent UCSD Benchmarks for Java .

Eugene Kuleshov
źródło
5
To był także interesujący artykuł: ibm.com/developerworks/java/library/j-jtp12214
John Nilsson
142
Nigdy też nie używaj System.currentTimeMillis (), chyba że jesteś w porządku z dokładnością + lub - 15 ms, co jest typowe dla większości kombinacji OS + JVM. Zamiast tego użyj System.nanoTime ().
Scott Carey
5
Trochę papieru z javaOne: azulsystems.com/events/javaone_2009/session/…
bestsss
93
Należy zauważyć, że System.nanoTime()nie ma gwarancji, że będzie dokładniejszy niż System.currentTimeMillis(). Jest gwarantowana tylko co najmniej tak dokładna. Zwykle jest jednak znacznie dokładniejszy.
Grawitacja
41
Głównym powodem, dla którego należy użyć System.nanoTime()zamiast tego System.currentTimeMillis()jest to, że ten pierwszy gwarantuje monotoniczny wzrost. Po odjęciu wartości zwrócone dwa currentTimeMilliswywołania mogą faktycznie dać negatywne wyniki, być może dlatego, że czas systemowy został dostosowany przez jakiegoś demona NTP.
Waldheinz
239

Wiem, że to pytanie zostało oznaczone jako odpowiedź, ale chciałem wspomnieć o dwóch bibliotekach, które pomagają nam pisać mikroskopy

Suwmiarka od Google

Wprowadzenie do samouczków

  1. http://codingjunkie.net/micro-benchmarking-with-caliper/
  2. http://vertexlabs.co.uk/blog/caliper

JMH z OpenJDK

Wprowadzenie do samouczków

  1. Unikanie pułapek porównawczych w JVM
  2. http://nitschinger.at/Using-JMH-for-Java-Microbenchmarking
  3. http://java-performance.info/jmh/
Aravind Yarram
źródło
37
+1 można by dodać jako regułę 8 przyjętej odpowiedzi: Zasada 8: ponieważ tak wiele rzeczy może pójść nie tak, prawdopodobnie powinieneś użyć istniejącej biblioteki zamiast próbować zrobić to sam!
assylias
8
@Pangea jmh jest obecnie prawdopodobnie lepszy od Calipera. Zobacz także: groups.google.com/forum/#!msg/mechanical-sympathy/m4opvy4xq3U/…
assylias
86

Ważnymi rzeczami dla testów porównawczych Java są:

  • Najpierw rozgrzej JIT, uruchamiając kod kilka razy, zanim go odmierzysz
  • Upewnij się, że uruchamiasz go wystarczająco długo, aby móc mierzyć wyniki w sekundach lub (lepiej) dziesiątkach sekund
  • Chociaż nie można wywoływać System.gc()między iteracjami, dobrym pomysłem jest uruchomienie go między testami, aby mieć nadzieję, że każdy test uzyska „czystą” przestrzeń pamięci do pracy. (Tak, gc()to raczej podpowiedź niż gwarancja, ale bardzo prawdopodobne jest , że naprawdę zbierze śmieci w moim doświadczeniu.)
  • Lubię wyświetlać iteracje i czas oraz wynik czasu / iteracji, który można skalować w taki sposób, że „najlepszy” algorytm otrzymuje wynik 1,0, a inne są oceniane w sposób względny. Oznacza to, że możesz uruchamiać wszystkie algorytmy przez długi czas, zmieniając zarówno liczbę iteracji, jak i czas, ale nadal uzyskując porównywalne wyniki.

Właśnie bloguję na temat projektowania frameworka w .NET. Mam kilka z wcześniejszych postów , które mogą być w stanie dać ci kilka pomysłów - nie wszystko będzie właściwe, oczywiście, ale niektóre z nich mogą być.

Jon Skeet
źródło
3
Drobny nitpick: IMO ”, aby każdy test dostał„ powinien być ”, aby każdy test mógł uzyskać”, ponieważ ten pierwszy sprawia wrażenie, że wywoływanie gc zawsze zwalnia nieużywaną pamięć.
Sanjay T. Sharma,
@ SanjayT.Sharma: Cóż, intencją jest to, że tak się dzieje. Chociaż nie jest to ściśle gwarantowane, w rzeczywistości jest to dość mocna wskazówka. Edycja będzie bardziej przejrzysta.
Jon Skeet
1
Nie zgadzam się z wywołaniem System.gc (). To wskazówka, to wszystko. Nawet „mam nadzieję, że coś zrobi”. Nigdy nie powinieneś tego tak nazywać. To jest programowanie, a nie sztuka.
gyorgyabraham
13
@gyabraham: Tak, to podpowiedź - ale zauważyłem, że zwykle ją zabiera. Jeśli więc nie lubisz używać System.gc(), jak możesz zminimalizować zbieranie śmieci w jednym teście ze względu na obiekty utworzone w poprzednich testach? Jestem pragmatyczny, nie dogmatyczny.
Jon Skeet
9
@gyabraham: Nie wiem, co rozumiesz przez „wielki powrót”. Czy potrafisz opracować i jeszcze raz - czy masz propozycję, by dać lepsze wyniki? Powiedziałem wyraźnie, że to nie jest gwarancja ...
Jon Skeet
48

jmh jest najnowszym dodatkiem do OpenJDK i został napisany przez niektórych inżynierów wydajności z Oracle. Z pewnością warte obejrzenia.

Jmh to uprząż Java do budowania, uruchamiania i analizowania testów porównawczych nano / mikro / makro napisanych w Javie i innych językach ukierunkowanych na JVM.

Bardzo ciekawe informacje zakopane w komentarzach do przykładowych testów .

Zobacz też:

assylias
źródło
1
Zobacz także ten post na blogu: psy-lob-saw.blogspot.com/2013/04/…, aby uzyskać szczegółowe informacje na temat rozpoczynania pracy z JMH.
Nitsan Wakart
Do Twojej wiadomości, JEP 230: Microbenchmark Suite to propozycja OpenJDK oparta na projekcie Java Microbenchmark Harness (JMH) . Nie zrobił cięcia dla Java 9, ale może zostać dodany później.
Basil Bourque,
23

Czy test porównawczy powinien mierzyć czas / iterację lub iteracje / czas i dlaczego?

To zależy od tego , co próbujesz przetestować.

Jeśli jesteś zainteresowany opóźnieniami , użyj czasu / iteracji, a jeśli jesteś zainteresowany przepustowością , użyj iteracji / czasu.

Peter Lawrey
źródło
16

Jeśli próbujesz porównać dwa algorytmy, wykonaj co najmniej dwa testy porównawcze dla każdego z nich, zmieniając kolejność. to znaczy:

for(i=1..n)
  alg1();
for(i=1..n)
  alg2();
for(i=1..n)
  alg2();
for(i=1..n)
  alg1();

Zauważyłem pewne zauważalne różnice (czasami 5-10%) w czasie wykonywania tego samego algorytmu w różnych przebiegach.

Upewnij się również, że n jest bardzo duże, aby czas działania każdej pętli wynosił co najmniej 10 sekund. Im więcej iteracji, tym bardziej znaczące dane w czasie testu i tym bardziej wiarygodne są dane.

Wyrko
źródło
5
Naturalnie zmiana kolejności wpływa na czas działania. Tutaj będą działać optymalizacje JVM i efekty buforowania. Lepiej jest „rozgrzać” optymalizację JVM, wykonać wiele przebiegów i porównać każdy test w innym JVM.
Mnementh
15

Upewnij się, że w jakiś sposób korzystasz z wyników obliczonych w kodzie testowym. W przeciwnym razie kod można zoptymalizować.

Peter Štibraný
źródło
13

Istnieje wiele możliwych pułapek pisania mikrokredytów w Javie.

Po pierwsze: musisz obliczyć za pomocą wszelkiego rodzaju zdarzeń, które zajmują mniej więcej czas: zbieranie śmieci, efekty buforowania (systemu operacyjnego dla plików i procesora dla pamięci), operacji we / wy itp.

Po drugie: nie można ufać dokładności zmierzonych czasów w bardzo krótkich odstępach czasu.

Po trzecie: JVM optymalizuje kod podczas wykonywania. Tak więc różne przebiegi w tej samej instancji JVM będą coraz szybsze.

Moje rekomendacje: Spraw, by Twój test porównawczy działał przez kilka sekund, co jest bardziej niezawodne niż czas działania przez milisekundy. Rozgrzej JVM (oznacza uruchomienie testu co najmniej raz bez mierzenia, że ​​JVM może uruchomić optymalizacje). I przeprowadź test porównawczy wiele razy (może 5 razy) i weź medianę. Uruchom każdy mikro-test porównawczy w nowej instancji JVM (wywołaj każdą nową testową Javę), w przeciwnym razie efekty optymalizacji JVM mogą wpłynąć na późniejsze testy. Nie wykonuj rzeczy, które nie są wykonywane w fazie rozgrzewania (ponieważ może to spowodować obciążenie klasy i rekompilację).

Memento
źródło
8

Należy również zauważyć, że ważna może być również analiza wyników mikroprocesora przy porównywaniu różnych wdrożeń. Dlatego test istotności należy wykonać .

Wynika to z faktu, że wdrożenie Amoże być szybsze przez większość przebiegów testu porównawczego niż wdrożenie B. Ale Amoże również mieć większy spread, więc zmierzona korzyść z wydajności Anie będzie miała żadnego znaczenia w porównaniu zB .

Ważne jest więc, aby poprawnie napisać i uruchomić mikroprocesor, ale także poprawnie go przeanalizować.

SpaceTrucker
źródło
8

Aby dodać do innych doskonałych porad, będę również pamiętać o następujących kwestiach:

W przypadku niektórych procesorów (np. Seria Intel Core i5 z TurboBoost) temperatura (i liczba obecnie używanych rdzeni, a także ich procent wykorzystania) wpływa na szybkość zegara. Ponieważ procesory są taktowane dynamicznie, może to wpłynąć na wyniki. Na przykład, jeśli masz aplikację jednowątkową, maksymalna prędkość zegara (z TurboBoost) jest wyższa niż dla aplikacji wykorzystującej wszystkie rdzenie. Może to zatem zakłócać porównania wydajności jedno- i wielowątkowej w niektórych systemach. Należy pamiętać, że temperatura i napięcia mają również wpływ na to, jak długo utrzymywana jest częstotliwość turbo.

Być może bardziej fundamentalny aspekt, nad którym masz bezpośrednią kontrolę: upewnij się, że mierzysz właściwą rzecz! Na przykład, jeśli używasz System.nanoTime()do testowania określonego fragmentu kodu, umieść wywołania zadania w miejscach, które mają sens, aby uniknąć mierzenia rzeczy, które Cię nie interesują. Na przykład nie rób:

long startTime = System.nanoTime();
//code here...
System.out.println("Code took "+(System.nanoTime()-startTime)+"nano seconds");

Problem polega na tym, że nie kończy się od razu, kiedy kod się skończy. Zamiast tego spróbuj wykonać następujące czynności:

final long endTime, startTime = System.nanoTime();
//code here...
endTime = System.nanoTime();
System.out.println("Code took "+(endTime-startTime)+"nano seconds");
Sina Madani
źródło
Tak, ważne jest, aby nie wykonywać niezwiązanej pracy w regionie czasowym, ale twój pierwszy przykład jest nadal w porządku. Jest tylko jedno wywołanie do println, a nie osobna linia nagłówka lub coś takiego, i System.nanoTime()należy to ocenić jako pierwszy krok w konstruowaniu ciągu arg dla tego wywołania. Kompilator nie może nic zrobić z pierwszym, czego nie może zrobić z drugim, i żaden z nich nawet nie zachęca ich do dodatkowej pracy przed zarejestrowaniem czasu zatrzymania.
Peter Cordes
7

http://opt.sourceforge.net/ Java Micro Benchmark - zadania kontrolne wymagane do określenia porównawczych charakterystyk wydajności systemu komputerowego na różnych platformach. Może służyć do kierowania decyzjami dotyczącymi optymalizacji i porównywania różnych implementacji Java.

Jurij
źródło
2
Wydaje się, że to tylko test porównawczy sprzętu JVM +, a nie dowolny fragment kodu Java.
Stefan L