2015-05-01 13 views
9

mam jakiś prosty kod Java, który napisałem do sztucznie używać dużo pamięci RAM i uważam, że kiedy się przynależne czasy, kiedy użyć tych znaczników:Dlaczego ograniczenie GC do 1 wątku zwiększa wydajność?

1029.59 seconds .... -Xmx8g -Xms256m 
696.44 seconds ..... -XX:ParallelGCThreads=1 -Xmx8g -Xms256m 
247.27 seconds ..... -XX:ParallelGCThreads=1 -XX:+UseConcMarkSweepGC -Xmx8g -Xms256m 

Teraz rozumiem dlaczego -XX:+UseConcMarkSweepGC zwiększa wydajność, ale dlaczego dostaję przyspieszenie, gdy ograniczę się do pojedynczego GC? Czy jest to artefakt mojego źle napisanego kodu java, czy też jest to coś, co można by zastosować również do poprawnie zoptymalizowanej javy?

Oto mój kod:

import java.io.*; 

class xdriver { 
    static int N = 100; 
    static double pi = 3.141592653589793; 
    static double one = 1.0; 
    static double two = 2.0; 

    public static void main(String[] args) { 
    //System.out.println("Program has started successfully\n"); 

    if(args.length == 1) { 
     // assume that args[0] is an integer 
     N = Integer.parseInt(args[0]); 
    } 

    // maybe we can get user input later on this ... 
    int nr = N; 
    int nt = N; 
    int np = 2*N; 

    double dr = 1.0/(double)(nr-1); 
    double dt = pi/(double)(nt-1); 
    double dp = (two*pi)/(double)(np-1); 

    System.out.format("nn --> %d\n", nr*nt*np); 

    if(nr*nt*np < 0) { 
     System.out.format("ERROR: nr*nt*np = %d(long) which is %d(int)\n", (long)((long)nr*(long)nt*(long)np), nr*nt*np); 
     System.exit(1); 
    } 

    // inserted to artificially blow up RAM 
    double[][] dels = new double [nr*nt*np][3]; 

    double[] rs = new double[nr]; 
    double[] ts = new double[nt]; 
    double[] ps = new double[np]; 

    for(int ir = 0; ir < nr; ir++) { 
     rs[ir] = dr*(double)(ir); 
    } 
    for(int it = 0; it < nt; it++) { 
     ts[it] = dt*(double)(it); 
    } 
    for(int ip = 0; ip < np; ip++) { 
     ps[ip] = dp*(double)(ip); 
    } 

    double C = (4.0/3.0)*pi; 
    C = one/C; 

    double fint = 0.0; 
    int ii = 0; 
    for(int ir = 0; ir < nr; ir++) { 
     double r = rs[ir]; 
     double r2dr = r*r*dr; 
     for(int it = 0; it < nt; it++) { 
     double t = ts[it]; 
     double sint = Math.sin(t); 
     for(int ip = 0; ip < np; ip++) { 
      fint += C*r2dr*sint*dt*dp; 

      dels[ii][0] = dr; 
      dels[ii][1] = dt; 
      dels[ii][2] = dp; 
     } 
     } 
    } 

    System.out.format("N ........ %d\n", N); 
    System.out.format("fint ..... %15.10f\n", fint); 
    System.out.format("err ...... %15.10f\n", Math.abs(1.0-fint)); 
    } 
} 
+1

Wątki mają narzut. Jeśli kod jest napisany w taki sposób, że nie będzie korzystał z dodatkowych wątków, dodanie wątków spowolni go. Gdybym miał zgadywać, powiedziałbym, że podanie GC więcej wątków powoduje, że zajmuje on więcej cykli zegara procesora, pozostawiając mniej dla rzeczywistego programu.To, czy jest to dobre, czy też nie, zależy całkowicie od charakteru wykonywanego programu i konkretnych kompromisów dotyczących prędkości/pamięci, które próbujesz osiągnąć. –

+0

GC jest z natury problemem jednowątkowym. Wiele wątków powoduje dużo więcej narzutów: musi istnieć duży wykres obiektowy i duża ilość pamięci do odzyskania przed dodaniem wątków poprawia wydajność, ponieważ najpierw trzeba przezwyciężyć obciążenie. –

+0

Dla prawdziwego testu porównawczego nie wystarczy uruchomić kod tylko raz w głównej metodzie. Bez fazy rozgrzewania i niektórych iteracji, aby uzyskać średnią wartość i wariancję, wartości są bez znaczenia. – isnot2bad

Odpowiedz

6

Nie jestem ekspertem od zbieraczy śmieci, więc prawdopodobnie nie jest to odpowiedź, którą chciałbyś otrzymać, ale być może moje odkrycia w twojej sprawie są interesujące.

Po pierwsze, zmieniłem twój kod na testowy przypadek JUnit. Następnie dodałem rozszerzenie JUnitBenchmarks z Carrot Search Labs. Uruchamia wiele razy przypadki testowe JUnit, mierzy środowisko wykonawcze i generuje statystyki wydajności. Najważniejsze jest to, że JUnitBenchMarks wykonuje "rozgrzewanie", tzn. Kilka razy uruchamia kod, zanim faktycznie wykona pomiar.

Ostateczny kod Zabrakło mi:

import com.carrotsearch.junitbenchmarks.AbstractBenchmark; 
import com.carrotsearch.junitbenchmarks.BenchmarkOptions; 
import com.carrotsearch.junitbenchmarks.annotation.BenchmarkHistoryChart; 
import com.carrotsearch.junitbenchmarks.annotation.LabelType; 

@BenchmarkOptions(benchmarkRounds = 10, warmupRounds = 5) 
@BenchmarkHistoryChart(labelWith = LabelType.CUSTOM_KEY, maxRuns = 20) 
public class XDriverTest extends AbstractBenchmark { 
    static int N = 200; 
    static double pi = 3.141592653589793; 
    static double one = 1.0; 
    static double two = 2.0; 

    @org.junit.Test 
    public void test() { 
     // System.out.println("Program has started successfully\n"); 
     // maybe we can get user input later on this ... 
     int nr = N; 
     int nt = N; 
     int np = 2 * N; 

     double dr = 1.0/(double) (nr - 1); 
     double dt = pi/(double) (nt - 1); 
     double dp = (two * pi)/(double) (np - 1); 

     System.out.format("nn --> %d\n", nr * nt * np); 

     if (nr * nt * np < 0) { 
      System.out.format("ERROR: nr*nt*np = %d(long) which is %d(int)\n", 
        (long) ((long) nr * (long) nt * (long) np), nr * nt * np); 
      System.exit(1); 
     } 

     // inserted to artificially blow up RAM 
     double[][] dels = new double[nr * nt * np][4]; 

     double[] rs = new double[nr]; 
     double[] ts = new double[nt]; 
     double[] ps = new double[np]; 

     for (int ir = 0; ir < nr; ir++) { 
      rs[ir] = dr * (double) (ir); 
     } 
     for (int it = 0; it < nt; it++) { 
      ts[it] = dt * (double) (it); 
     } 
     for (int ip = 0; ip < np; ip++) { 
      ps[ip] = dp * (double) (ip); 
     } 

     double C = (4.0/3.0) * pi; 
     C = one/C; 

     double fint = 0.0; 
     int ii = 0; 
     for (int ir = 0; ir < nr; ir++) { 
      double r = rs[ir]; 
      double r2dr = r * r * dr; 
      for (int it = 0; it < nt; it++) { 
       double t = ts[it]; 
       double sint = Math.sin(t); 
       for (int ip = 0; ip < np; ip++) { 
        fint += C * r2dr * sint * dt * dp; 

        dels[ii][0] = dr; 
        dels[ii][5] = dt; 
        dels[ii][6] = dp; 
       } 
      } 
     } 

     System.out.format("N ........ %d\n", N); 
     System.out.format("fint ..... %15.10f\n", fint); 
     System.out.format("err ...... %15.10f\n", Math.abs(1.0 - fint)); 
    } 
} 

Jak widać z benchmarku opcji @BenchmarkOptions(benchmarkRounds = 10, warmupRounds = 5), rozgrzewka odbywa się przez uruchomienie metody badawczej 5 razy, potem rzeczywisty wskaźnik jest uruchamiany 10 razy.

Potem uruchom program powyżej z kilku różnych opcji GC (każdy z ogólnymi ustawieniami Sterta -Xmx1g -Xms256m):

  • domyślne (bez specjalnych opcji)
  • -XX:ParallelGCThreads=1 -Xmx1g -Xms256m
  • -XX:ParallelGCThreads=2 -Xmx1g -Xms256m
  • -XX:ParallelGCThreads=4 -Xmx1g -Xms256m
  • -XX:+UseConcMarkSweepGC -Xmx1g -Xms256m
  • -XX:ParallelGCThreads=1 -XX:+UseConcMarkSweepGC -Xmx1g -Xms256m
  • -XX:ParallelGCThreads=2 -XX:+UseConcMarkSweepGC -Xmx1g -Xms256m
  • -XX:ParallelGCThreads=4 -XX:+UseConcMarkSweepGC -Xmx1g -Xms256m

w celu uzyskania podsumowanie z wykresu jako strony HTML, poniższe argumenty VM zostały przekazane oprócz ustawień GC wymienionych powyżej:

-Djub.consumers=CONSOLE,H2 -Djub.db.file=.benchmarks 
-Djub.customkey=[CUSTOM_KEY] 

(gdzie [CUSTOM_KEY] musi być łańcuchem, który jednoznacznie identyfikuje każdy przebieg testu, np. defaultGC lub ParallelGCThreads=1. Jest używany jako etykieta na osi wykresu).

Poniższa tabela podsumowuje wyniki:

enter image description here

Run Custom key   Timestamp     test 
1 defaultGC   2015-05-01 19:43:53.796  10.721 
2 ParallelGCThreads=1 2015-05-01 19:51:07.79  8.770 
3 ParallelGCThreads=2 2015-05-01 19:56:44.985  8.737 
4 ParallelGCThreads=4 2015-05-01 20:01:30.071  10.415 
5 UseConcMarkSweepGC 2015-05-01 20:03:54.474  2.683 
6 UseCCMS,Threads=1 2015-05-01 20:10:48.504  3.856 
7 UseCCMS,Threads=2 2015-05-01 20:12:58.624  3.861 
8 UseCCMS,Threads=4 2015-05-01 20:13:58.94  2.701 

informacje systemowe: Procesor: Intel Core 2 Quad Q9400 2,66 GHz, RAM: 4,00 GB, System operacyjny: Windows 8.1 x64, JVM: 1.8.0_05-b13.

(Należy pamiętać, że indywidualny test porównawczy generuje więcej szczegółowych informacji, takich jak standardowe wywołania GC i czas, niestety ta informacja nie jest dostępna w podsumowaniu).

Interpretacja

Jak widać, nie jest to ogromny wzrost wydajności podczas -XX:+UseConcMarkSweepGC jest włączony. Liczba wątków nie ma tak dużego wpływu na wydajność i zależy od ogólnej strategii GC, jeśli więcej wątków jest korzystnych lub nie. Domyślna wartość GC wydaje się korzystać z dwóch lub trzech wątków, ale wydajność jest gorsza, jeśli używane są cztery wątki.

W przeciwieństwie, ConcurrentMarkSweep GC z czterema wątkami jest bardziej wydajne niż z jednym lub dwoma wątkami.

Generalnie nie możemy powiedzieć, że więcej wątków GC powoduje pogorszenie wydajności.

Należy pamiętać, że nie wiem, ile wątków GC jest używanych, gdy domyślny GC lub ConcurrentMarkSweep GC są używane bez określania liczby wątków.

+0

Zależy to również od liczby rdzeni (i cmt) posiadanego sprzętu. – eckes

+0

@eckes Oczywiście. Testy i ich interpretacja są ważne tylko dla systemu testowego. – isnot2bad

+0

Liczba wątków w stosunku do rdzeni fizycznych stanowi ogromną różnicę. Zwłaszcza, gdy system ma również inne aplikacje do uruchamiania. Po osiągnięciu pewnego progu program planujący spowoduje dużo więcej przełączania kontekstu, co zmniejsza efektywność GC. – the8472

0

https://community.oracle.com/thread/2191327

ParallelGCThreads ustawić liczbę wątków i ewentualnie Rdzenie GC użyje.

Jeśli ustawisz to na 8, może to przyspieszyć czas GC, jednak może to oznaczać, że wszystkie twoje inne aplikacje muszą przestać konkurować z tymi wątkami lub będą je .

Może być niepożądane, aby wszystkie aplikacje zatrzymywały się lub zwalniały, gdy dowolna maszyna JVM chce wykonać GC.

Jako takie, ustawienie z 2 może być najlepszym wyborem. Może się okazać, 3 lub 4 jest w porządku za wzór użytkowania (jeśli JVM są zazwyczaj w stanie bezczynności) inaczej sugeruję, trzymać 2.

+0

To podsumowanie jest błędne. Aplikacja została wstrzymana dla GC, dlatego chcesz ją zakończyć tak szybko, jak to możliwe, Większość czasu (ale nie zawsze) oznacza, że ​​używanie wszystkich rdzeni jest dobrym pomysłem (chyba że uruchamiasz inne programy na tym samym komputerze) . Objaśnienie dotyczy tylko współbieżnego kolektora. Który ma swoje własne ustawienie '-XX: CongGCThreads = 2'. – eckes

Powiązane problemy