gpt4 book ai didi

java - 为什么两个独立的循环比一个快?

转载 作者:IT老高 更新时间:2023-10-28 20:35:58 25 4
gpt4 key购买 nike

我想了解 Java 对连续 for 循环做了哪些优化。更准确地说,我正在尝试检查是否执行了循环融合。从理论上讲,我期望这种优化不是自动完成的,并且期望确认融合版本比具有两个循环的版本更快。

但是,在运行基准测试后,结果显示两个独立(连续)循环比一个循环完成所有工作要快。

我已经尝试使用 JMH 创建基准测试并获得了相同的结果。

我使用了javap命令,它显示为具有两个循环的源文件生成的字节码实际上对应于正在执行的两个循环(没有执行循环展开或其他优化)。

正在测量 BenchmarkMultipleLoops.java 的代码:

private void work() {
List<Capsule> intermediate = new ArrayList<>();
List<String> res = new ArrayList<>();
int totalLength = 0;

for (Capsule c : caps) {
if(c.getNumber() > 100000000){
intermediate.add(c);
}
}

for (Capsule c : intermediate) {
String s = "new_word" + c.getNumber();
res.add(s);
}

//Loop to assure the end result (res) is used for something
for(String s : res){
totalLength += s.length();
}

System.out.println(totalLength);
}

正在测量 BenchmarkSingleLoop.java 的代码:

private void work(){
List<String> res = new ArrayList<>();
int totalLength = 0;

for (Capsule c : caps) {
if(c.getNumber() > 100000000){
String s = "new_word" + c.getNumber();
res.add(s);
}
}

//Loop to assure the end result (res) is used for something
for(String s : res){
totalLength += s.length();
}

System.out.println(totalLength);
}

这里是 Capsule.java 的代码:

public class Capsule {
private int number;
private String word;

public Capsule(int number, String word) {
this.number = number;
this.word = word;
}

public int getNumber() {
return number;
}

@Override
public String toString() {
return "{" + number +
", " + word + '}';
}
}

capsArrayList<Capsule>一开始有 2000 万个元素是这样填充的:

private void populate() {
Random r = new Random(3);

for(int n = 0; n < POPSIZE; n++){
int randomN = r.nextInt();
Capsule c = new Capsule(randomN, "word" + randomN);
caps.add(c);
}
}

在测量之前,执行预热阶段。

我对每个基准运行了 10 次,换句话说,work()每个基准测试执行 10 次方法,完成的平均时间如下所示(以秒为单位)。每次迭代后,GC 都会执行一些 hibernate :

  • 多个循环:4.9661 秒
  • 单循环:7.2725 秒

在 Intel i7-7500U (Kaby Lake) 上运行的 OpenJDK 1.8.0_144。

为什么 MultipleLoops 版本比 SingleLoop 版本更快,即使它必须遍历两种不同的数据结构?

更新 1:

如评论中所建议,如果我更改实现以计算 totalLength在生成字符串时,避免创建 res list ,单循环版本变得更快。

但是,该变量仅被引入,以便在创建结果列表后完成一些工作,以避免在未对它们进行任何操作时丢弃元素。

换句话说,预期的结果是生成最终列表。但这个建议有助于更好地理解正在发生的事情。

结果:

  • 多个循环:0.9339 秒
  • 单循环:0.66590005 秒

更新 2:

这是我用于 JMH 基准测试的代码的链接: https://gist.github.com/FranciscoRibeiro/2d3928761f76e4f7cecfcfcdf7fc96d5

结果:

  • 多个循环:7.397 秒
  • 单循环:8.092 秒

最佳答案

我调查了这个“现象”,看起来像是得到了答案。
让我们将 .jvmArgs("-verbose:gc") 添加到 JMHs OptionsBuilder。 1 次迭代的结果:

Single Loop: [Full GC (Ergonomics) [PSYoungGen: 2097664K->0K(2446848K)] [ParOldGen: 3899819K->4574771K(5592576K)] 5997483K->4574771K(8039424K), [Metaspace: 6208K->6208K(1056768K)], 5.0438301 secs] [Times: user=37.92 sys=0.10, real=5.05 secs] 4.954 s/op

Multiple Loops: [Full GC (Ergonomics) [PSYoungGen: 2097664K->0K(2446848K)] [ParOldGen: 3899819K->4490913K(5592576K)] 5997483K->4490913K(8039424K), [Metaspace: 6208K->6208K(1056768K)], 3.7991573 secs] [Times: user=26.84 sys=0.08, real=3.80 secs] 4.187 s/op

JVM 为 GC 花费了大量的 CPU 时间。每 2 次测试运行一次,JVM 必须进行 Full GC(将 600Mb 移动到 OldGen 并从之前的周期中收集 1.5Gb 的垃圾)。两个垃圾收集器都完成了相同的工作,但多循环测试用例的应用时间减少了约 25%。如果我们将 POPSIZE 减少到 10_000_000 或添加到 bh.consume() Thread.sleep(3000) 之前,或者添加 - XX:+UseG1GC 到 JVM args,然后多循环提升效果消失了。我用 .addProfiler(GCProfiler.class) 再次运行它。主要区别:

Multiple Loops: gc.churn.PS_Eden_Space 374.417 ± 23 MB/sec

Single Loop: gc.churn.PS_Eden_Space 336.037 MB/sec ± 19 MB/sec

我认为,我们在这种特定情况下看到了加速,因为旧的比较和交换 GC 算法在多次测试运行时存在 CPU 瓶颈,并且从早期运行中收集垃圾使用了额外的“无意义”循环。如果您有足够的 RAM,则使用 @Threads(2) 进行复制会更容易。如果您尝试分析 Single_Loop 测试,它看起来像这样:

profiling

关于java - 为什么两个独立的循环比一个快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48952494/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com