在较新的jvm上,对for循环中数组中所有元素求和的性能(吞吐量)比在Java1.8.0JDK的jvm上慢。我执行了jhm基准测试(下图)。在每次测试之前,源代码由提供的javac.exe编译并由java.exe运行,这两个二进制文件都由选定的jdk提供。测试是在windows10上执行的,由powershell脚本启动,没有任何程序在后台运行(没有其他jvm)。这台电脑配备了32gb的ram,因此没有使用硬盘上的虚拟内存。
阵列中有10m个元素:
阵列中的100m元素:
我的测试源代码:
@Param({"10000000", "100000000"})
public static int ELEMENTS;
public static void main(String[] args) throws RunnerException, IOException {
File outputFile = new File(args[0]);
int javaMajorVersion = Integer.parseInt(System.getProperty("java.version").split("\\.")[0]);
ChainedOptionsBuilder builder = new OptionsBuilder()
.include(IteratingBenchmark.class.getSimpleName())
.mode(Mode.Throughput)
.forks(2)
.measurementTime(TimeValue.seconds(10))
.measurementIterations(50)
.warmupTime(TimeValue.seconds(2))
.warmupIterations(10)
.resultFormat(ResultFormatType.SCSV)
.result(outputFile.getAbsolutePath());
if (javaMajorVersion > 8) {
builder = builder.jvmArgs("-Xms20g", "-Xmx20g", "--enable-preview");
} else {
builder = builder.jvmArgs("-Xms20g", "-Xmx20g");
}
new Runner(builder.build()).run();
}
@Benchmark
public static void cStyleForLoop(Blackhole bh, MockData data) {
long sum = 0;
for (int i = 0; i < data.randomInts.length; i++) {
sum += data.randomInts[i];
}
bh.consume(sum);
}
@State(Scope.Thread)
public static class MockData {
private int[] randomInts = new int[ELEMENTS];
@Setup(Level.Iteration)
public void setup() {
Random r = new Random();
this.randomInts = Stream.iterate(r.nextInt(), i -> i + r.nextInt(1022) + 1).mapToInt(Integer::intValue).limit(ELEMENTS).toArray();
}
}
原始数据:
JDK 1.8.0_241:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;331,446104;5,563589;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;33,757268;0,431403;"ops/s";100000000
JDK 11.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;322,728461;4,823611;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;31,075948;0,062830;"ops/s";100000000
JDK 12.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;322,914782;4,450969;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;31,095232;0,075051;"ops/s";100000000
JDK 13.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;325,103055;4,933257;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;31,228403;0,067954;"ops/s";100000000
JDK 14.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;300,861148;0,443404;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;29,863602;0,035781;"ops/s";100000000
OpenJDK 14.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;300,781930;0,481579;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;29,873509;0,033055;"ops/s";100000000
OpenJDK 15:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;343,530895;0,445551;"ops/s";10000000
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;100;34,287083;0,035028;"ops/s";100000000
为什么java的新版本比1.8慢(OpenJDK15除外),有什么有效的解释吗?
更新1:
我对不同的xmx/xms值运行相同的测试(对于每个测试xmx==xms),结果如下:
更新2:
首先,我变了 Level.Iteration
至 Level.Trial
.
其次,我强制g1垃圾收集器。
第三,xmx/xms被设置为8gb
结果:
原始数据:
JDK 1.8.0_241:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;33,760346;0,089646;"ops/s";100000000
JDK 11.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;31,075120;0,086171;"ops/s";100000000
JDK 12.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;31,173939;0,044176;"ops/s";100000000
JDK 13.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;31,219283;0,062329;"ops/s";100000000
JDK 14.0.1:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;29,808609;0,072664;"ops/s";100000000
OpenJDK 14.0.2:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;29,845817;0,074315;"ops/s";100000000
OpenJDK 15:
"Benchmark";"Mode";"Threads";"Samples";"Score";"Score Error (99,9%)";"Unit";"Param: ELEMENTS"
"benchmark.IteratingBenchmark.cStyleForLoop";"thrpt";1;15;34,310620;0,087412;"ops/s";100000000
更新3:
我制作了包含基准源代码的github存储库,并编写了脚本来使用我使用的jmh参数执行基准测试,该脚本自动生成png格式的绘图。另外,我在其他机器(linux)上执行了基准测试。
linux机器的结果似乎更乐观:
不幸的是,在我的windows机器上,结果仍然显示性能下降(jdk15除外)。
更新4:结果 -XX:-UseCountedLoopSafepoints
:
1条答案
按热度按时间tnkciper1#
即使从github逐字复制基准测试并使用相同的参数运行,我仍然无法再现结果。在我的环境中,jdk14的执行速度与jdk8一样快(甚至快一点)。因此,在这个答案中,我将基于编译代码的反汇编来分析两个版本之间的差异。
首先,让我们看一下同一供应商最新的openjdk构建。
在这里,我比较liberica jdk 8u265+1和liberica jdk 14.0.2+13 for windows 64位。
jmh分数如下:
现在让我们用内置的
-prof xperfasm
剖析器,看看最热门的部分基准拆解。预计约99.5%的cpu时间花在c2编译上cStyleForLoop
方法。jdk 8上最热的区域
JDK14上最热的区域
如我们所见,循环体在两个JDK上的编译方式类似:
展开8个循环迭代;
数组中有8个加载没有边界检查,后面是8个
add
说明书;加载的顺序有点不同,但是所有的地址都共享相同的缓存线或相邻的缓存线。
关键的区别在于,在JDK14上,循环迭代被分成两个嵌套块。这是JDK10中出现的环形条带开采优化的结果。这种优化的思想是将计数的循环分为不带safepoint轮询的热内部部分和带safepoint轮询指令的外部部分。
c2jit将循环转换为
请注意,jdk8版本在counted循环中根本没有safepoint轮询。一方面,这可以使循环运行得更快。但另一方面,这实际上不利于低延迟应用程序,因为暂停时间可能会随着整个循环的持续时间而增加。
JDK14在循环中插入一个safepoint轮询。这可能是您观察到的速度减慢的一个原因,但我不太相信这一点,因为由于循环条带开采优化,safepoint轮询在8000次迭代中只执行一次。
要验证这一点,可以使用禁用safepoint轮询
-XX:-UseCountedLoopSafepoints
jvm选项。在本例中,jdk14编译版本看起来与jdk8版本几乎相同。基准分数也是如此。