为什么将GC限制为1个线程可以提高性能?

时间:2015-05-01 16:52:28

标签: java multithreading garbage-collection

我有一些简单的java代码,我写这些代码是为了人为地使用大量的RAM,我发现当我使用这些标志时得到相关的时间:

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

现在,我理解为什么-XX:+UseConcMarkSweepGC会提高性能,但为什么在限制单线程GC时会获得加速?这是我编写糟糕的java代码的工件,还是适用于正确优化的java的东西?

这是我的代码:

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

2 个答案:

答案 0 :(得分:7)

我不是垃圾收集者的专家,所以这可能不是你想要的答案,但也许我对你的问题的发现很有意思。

首先,我已将您的代码更改为JUnit测试用例。然后我添加了JUnitBenchmarksCarrot Search Labs扩展名。它多次运行JUnit测试用例,测量运行时,并输出一些性能统计信息。最重要的是JUnitBenchMarks确实“预热”,即它在实际测量之前多次运行代码。

我运行的最终代码:

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

从基准测试选项@BenchmarkOptions(benchmarkRounds = 10, warmupRounds = 5)可以看出,通过运行测试方法5次来完成预热,然后实际基准测试运行10次。

然后我使用几个不同的GC选项运行上面的程序(每个选项都具有-Xmx1g -Xms256m的常规堆设置):

  • 默认(无特殊选项)
  • -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

为了获得图表作为HTML页面的摘要,除了上面提到的GC设置外,还传递了以下VM参数:

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

(其中[CUSTOM_KEY]必须是唯一标识每个基准运行的字符串,例如defaultGCParallelGCThreads=1。它用作图表轴上的标签。)

下表总结了结果:

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

系统信息:CPU:Intel Core 2 Quad Q9400,2.66 GHz,RAM:4.00 GB,操作系统:Windows 8.1 x64,JVM:1.8.0_05-b13。

(请注意,单个基准测试会输出更多详细信息,例如标准派生GC调用和时间;遗憾的是,此信息在摘要中不可用。)

<强>解释

如您所见,启用-XX:+UseConcMarkSweepGC后会有巨大的性能提升。线程数不会对性能产生太大影响,并且如果有更多线程是有利的,则它取决于通用GC策略。默认GC似乎从两个或三个线程中获利,但如果使用四个线程,性能会变差。

相反,具有四个线程的 ConcurrentMarkSweep GC 比一个或两个线程更具性能。

所以一般来说,我们不能说更多的GC线程会让性能变差。

请注意,我不知道在使用默认GC或 ConcurrentMarkSweep GC 而未指定线程数时使用了多少个GC线程。

答案 1 :(得分:0)

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

  

ParallelGCThreads设置线程数并可能为GC提供核心   将使用。

     

如果将其设置为8,则可以加快GC时间   它可能意味着您的所有其他应用程序必须停止或将要   与这些线程竞争。

     

拥有所有你的可能是不可取的   当任何JVM想要GC时,应用程序停止或减速。

     

因此,a   设置为2可能是您的最佳选择。你可能会发现3或4很好   对于你的使用模式(如果你的JVM通常是空闲的),否则我   建议,坚持2。