【发布时间】:2018-08-03 18:48:47
【问题描述】:
我想了解 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 + '}';
}
}
caps 是一个 ArrayList<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 都会执行一些休眠:
- 多个循环:4.9661 秒
- 单循环:7.2725 秒
在 Intel i7-7500U (Kaby Lake) 上运行的 OpenJDK 1.8.0_144。
为什么 MultipleLoops 版本比 SingleLoop 版本更快,即使它必须遍历两种不同的数据结构?
更新 1:
按照 cmets 中的建议,如果我在生成字符串时更改实现以计算 totalLength,避免创建 res 列表,单循环版本会变得更快。
但是,该变量仅被引入,以便在创建结果列表后完成一些工作,以避免在未对它们进行任何操作时丢弃元素。
换句话说,预期的结果是生成最终列表。但这个建议有助于更好地理解正在发生的事情。
结果:
- 多个循环:0.9339 秒
- 单循环:0.66590005 秒
更新 2:
这是我用于 JMH 基准测试的代码的链接: https://gist.github.com/FranciscoRibeiro/2d3928761f76e4f7cecfcfcdf7fc96d5
结果:
- 多个循环:7.397 秒
- 单循环:8.092 秒
【问题讨论】:
-
如果你直接增加
totalSize,跳过创建result列表会发生什么? -
我猜是因为分支预测。 stackoverflow.com/questions/11227809/…
-
刚刚尝试重现您的结果但没有成功。对我来说,两个循环都有大约。同样的表现,见gist.github.com/l-wi/20a923af1ee707e885e087e136af0bfe。您(或我的)基准代码是否存在缺陷?
-
请提供 MCVE。强调 C ... 完成。您说:“我已经尝试使用 JMH 创建基准测试并得到相同的结果。” 那么您应该将问题中的代码替换为基于 JMH 的版本和相应的结果。现在,有人说他们无法重现您的结果,但我们/他们不知道他们是否运行与您相同的基准测试。
-
先尝试进行 SingleLoops 测试。可能是您首先运行的测试在您第二次运行的测试期间创建了 GC 工作。
标签: java performance optimization benchmarking microbenchmark