2012-01-19 15 views
12

Gram z kawałkiem kodu obliczającym czas potrzebny do obliczenia kodu Java, aby poczuć wydajność lub nieefektywność niektórych funkcji Javy. Robiąc tak utknąłem teraz z naprawdę dziwnym efektem, którego po prostu nie potrafię sobie wytłumaczyć. Może ktoś z was pomoże mi to zrozumieć.Wydajność Java

public class PerformanceCheck { 

public static void main(String[] args) { 
    List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>(); 

    int maxTimes = 1000000000; 

    for (int i=0;i<10;i++) { 
     long time = System.currentTimeMillis(); 

     for (int times=0;times<maxTimes;times++) { 
      // PERFORMANCE CHECK BLOCK START 

      if (removeList.size() > 0) { 
       testFunc(3); 
      } 

      // PERFORMANCE CHECK BLOCK END 
     } 

     long timeNow = System.currentTimeMillis(); 
     System.out.println("time: " + (timeNow - time)); 
    } 
} 

private static boolean testFunc(int test) { 
    return 5 > test; 
} 

} 

Zaczynając skutkuje to stosunkowo długi czas obliczeń (pamiętaj removeList jest pusty, więc testFunc nawet nie nazywa):

time: 2328 
time: 2223 
... 

podczas wymiany czegokolwiek kombinacji removeList.size()> 0 i testFunc (3) z czymkolwiek innym ma lepsze wyniki. Na przykład:

... 
if (removeList.size() == 0) { 
    testFunc(3); 
} 
... 

Wyniki w (testFunc nazywa każdym razem):

time: 8 
time: 7 
time: 0 
time: 0 

Nawet wzywając obie funkcje niezależne od siebie wyników w dolnej czasu obliczeń:

... 
if (removeList.size() == 0); 
    testFunc(3); 
... 

Wynik:

time: 6 
time: 5 
time: 0 
time: 0 
... 

Tylko ta szczególna kombinacja w moim pierwszym przykładzie trwa tak długo. To mnie irytuje i naprawdę chciałbym to zrozumieć. Co jest takiego specjalnego w tym?

Dzięki.

Dodatek:

Zmiana testFunc() w pierwszym przykładzie

if (removeList.size() > 0) { 
       testFunc(times); 
} 

do czegoś innego, jak

private static int testFunc2(int test) { 
    return 5*test; 
} 

Spowoduje być szybko ponownie.

+0

Przeprowadziłeś ten test więcej niż raz, w różnych zamówieniach, prawda? Kolejność nie powinna mieć znaczenia, ale tylko po to, aby się upewnić. Czy otrzymujesz (o) te same wyniki z nanoTime, jak sugerowano poniżej? – prelic

+5

Tego rodzaju mikro-benchmark jest całkiem złym pomysłem. Zdaj sobie również sprawę, że JIT może podejmować różne decyzje w dowolnym momencie i całkowicie optymalizować swój kod, jeśli zdaje sobie sprawę, że nic się nie dzieje. –

+1

Powinieneś użyć [System.nanoTime()] (http://docs.oracle.com/javase/6/docs/api/java/lang/System.html#nanoTime%28%29) do pomiaru wykonania kodu w Javie . Jest bardziej precyzyjny. więcej dyskusji [w tym pytaniu] (http://stackoverflow.com/questions/351565/system-currenttimemillement-vs-system-nanotime) – paislee

Odpowiedz

0

Te testy porównawcze są trudne, ponieważ kompilatory są tak cholernie inteligentne. Jedno przypuszczenie: Ponieważ wynik metody testFunc() jest ignorowany, kompilator może ją całkowicie zoptymalizować. Dodaj licznik, coś jak

if (testFunc(3)) 
    counter++; 

I właśnie dla dokładności, zrobić System.out.println(counter) na końcu.

+0

Ale to sprawi, że pierwsza wersja będzie szybsza, ale tak nie jest. –

+2

Może to być zamówienie zgodnie z sugestią @prelic. Z jakiegoś powodu JIT nie zoptymalizował połączenia po raz pierwszy, ale zorientował się po raz drugi. – user949300

+0

Uruchomiłem to na dwa sposoby lokalnie i to nie zmienia rzeczy. –

3

To naprawdę zaskakujące. Wygenerowany kod bajtowy jest identyczny z wyjątkiem warunkowego, który jest ifle vs ifne.

Wyniki są znacznie bardziej sensowne, jeśli wyłączysz JIT pod numerem -Xint. Druga wersja jest 2x wolniejsza. Ma to związek z optymalizacją JIT.

Zakładam, że może zoptymalizować czek w drugim przypadku, ale nie pierwszy (z jakiegokolwiek powodu). Nawet jeśli oznacza to, że działa funkcja, brak tego warunkowego powoduje, że rzeczy są znacznie szybsze. Pozwala uniknąć straganów z rurociągami i tak dalej.

+0

Chodzi o to, że zależy to również od metody testFunc(). Zamień testFunc() pierwszej wersji na cokolwiek innego, na przykład: \t prywatny static int testFunc2 (test int) { \t \t powrót 5 * test; \t} Spowoduje to obniżenie czasu obliczeń. –

1

Cóż, cieszę się, że nie muszę radzić sobie z optymalizacją wydajności Java. Próbowałem tego sam z Java JDK 7 64-bit. Wyniki są arbitralne;).Nie ma znaczenia, które listy używam lub czy buforuję wynik size() przed wejściem do pętli. Całkowite wymazanie funkcji testowej nie ma prawie żadnej różnicy (więc nie może być również trafieniem w fazie rozgałęzień). Flagi optymalizacji zwiększają wydajność, ale są dowolne.

Jedyną logiczną konsekwencją jest to, że kompilator JIT może czasami zoptymalizować instrukcję (co nie jest trudne), ale wydaje się dość arbitralny. Jeden z wielu powodów, dla których preferuję języki takie jak C++, gdzie zachowanie jest co najmniej deterministyczne, nawet jeśli czasami jest arbitralne.

BTW w najnowszym Eclipse, jak to zawsze było w systemie Windows, działa ten kod za pośrednictwem IDE „Run” (bez debugowania) jest 10 razy wolniejsze niż uruchomienie go z konsoli, tyle o tym, że ...

+0

+1 dla deterministycznego, ale arbitralnego –

1

Gdy kompilator środowiska wykonawczego może stwierdzić, że wartość testFunc jest stała, uważam, że nie ocenia on pętli, co wyjaśnia przyspieszenie.

Gdy stan to removeList.size() == 0, funkcja testFunc(3) zostaje oceniona na stałą. Gdy warunek jest removeList.size() != 0, wewnętrzny kod nigdy nie zostanie oceniony, więc nie można go przyspieszyć. Można zmodyfikować kod w następujący sposób:

for (int times = 0; times < maxTimes; times++) { 
      testFunc(); // Removing this call makes the code slow again! 
      if (removeList.size() != 0) { 
       testFunc(); 
      } 
     } 

private static boolean testFunc() { 
    return testFunc(3); 
} 

Kiedy testFunc() nie jest początkowo zwany kompilator wykonawcze nie zdaje sobie sprawy, że testFunc() ma wartość stałą, więc nie można zoptymalizować pętli.

Niektóre funkcje, takie jak

private static int testFunc2(int test) { 
    return 5*test; 
} 

kompilator może próbuje wstępnie Optymalizacja (przed wykonaniem), ale najwyraźniej nie dla przypadku parametr jest przekazywana w postaci liczby całkowitej, oceniano w sposób warunkowy.

Twój odniesienia powraca razy jak

time: 107 
time: 106 
time: 0 
time: 0 
... 

co sugeruje, że w ciągu 2 iteracji zewnętrznej pętli dla kompilatora wykonawczego do końca optymalizacji. Kompilacja z flagą -server prawdopodobnie zwróci wszystkie 0 w benchmarku.

2

Chociaż nie jest to bezpośrednio związane z tym pytaniem, w ten sposób prawidłowo przeprowadzi się analizę porównawczą kodu za pomocą suwmiarki. Poniżej znajduje się zmodyfikowana wersja twojego kodu, aby działała z Suwmiarką. Wewnętrzne pętle musiały zostać zmodyfikowane, aby VM ich nie optymalizowała. Zaskakująco sprytnie zdajemy sobie sprawę, że nic się nie dzieje.

Istnieje również wiele niuansów podczas testowania kodu Java. Napisałem o niektórych problemach, które napotkałem pod numerem Java Matrix Benchmark, na przykład o tym, jak przeszłe historie mogą wpływać na bieżące wyniki. Unikniesz wielu z tych problemów za pomocą Suwmiarki.

  1. http://code.google.com/p/caliper/
  2. Benchmarking issues with Java Matrix Benchmark

    public class PerformanceCheck extends SimpleBenchmark { 
    
    public int timeFirstCase(int reps) { 
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>(); 
        removeList.add(new PerformanceCheck()); 
        int ret = 0; 
    
        for(int i = 0; i < reps; i++) { 
         if (removeList.size() > 0) { 
          if(testFunc(i)) 
           ret++; 
         } 
        } 
    
        return ret; 
    } 
    
    public int timeSecondCase(int reps) { 
        List<PerformanceCheck> removeList = new LinkedList<PerformanceCheck>(); 
        removeList.add(new PerformanceCheck()); 
        int ret = 0; 
    
        for(int i = 0; i < reps; i++) { 
         if (removeList.size() == 0) { 
          if(testFunc(i)) 
           ret++; 
         } 
        } 
    
        return ret; 
    } 
    
    private static boolean testFunc(int test) { 
        return 5 > test; 
    } 
    
    public static void main(String[] args) { 
        Runner.main(PerformanceCheck.class, args); 
    } 
    } 
    

WYJŚCIE:

0% Scenario{vm=java, trial=0, benchmark=FirstCase} 0.60 ns; σ=0.00 ns @ 3 trials 
50% Scenario{vm=java, trial=0, benchmark=SecondCase} 1.92 ns; σ=0.22 ns @ 10 trials 

benchmark ns linear runtime 
FirstCase 0.598 ========= 
SecondCase 1.925 ============================== 

vm: java 
trial: 0 
+0

Nie sądzę, że pytanie brzmi: jak mikrobańczyć, ale wyjaśniając zaobserwowane zachowanie. Ale trafiłeś to po drodze; to JIT optymalizuje coś w nieoczekiwany sposób. –

+0

Tak, zgadzam się. Pomyślałem, że mogę wskazać właściwą drogę, ponieważ nawet po tym, jak ten kod zostanie wyjaśniony, nadal będzie dziwna fluktuacja ze względu na sposób zestawienia testu. –

+0

Dzięki za cynk z zaciskiem. Wykorzystam to w przyszłości. Nadal pozostaje pytanie, dlaczego Java działa w ten sposób.W twoim przykładzie zmieniłeś zachowanie, dodając obiekt do listy. Teraz klauzula if 'timeSecondCase' zwraca wartość false. I nadal jest to jedna wersja, w której nie jest wywoływana funkcja testFunc(), która jest wolniejsza. –

1

Czasy są nieprawdopodobnie szybko za iteracji.Oznacza to, że JIT wykrył, że twój kod nic nie robi i wyeliminował go. Subtelne zmiany mogą wprowadzać w błąd JIT i nie mogą ustalić, że kod nic nie robi i zajmuje to trochę czasu.

Jeśli zmienisz test, aby zrobić coś nieznacznie użytecznego, różnica zniknie.