【发布时间】:2020-03-18 14:43:56
【问题描述】:
考虑以下两个长度为 2 的数组的 sn-ps 代码:
boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
和
boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
我会假设这两个部分的性能在充分热身后应该是相似的。
我已经使用 JMH 微基准测试框架检查了这一点,如所述,例如here 和 here 并观察到第二个 sn-p 快 10% 以上。
问题:为什么 Java 没有使用基本的循环展开技术优化我的第一个 sn-p?
我特别想了解以下内容:
- 我可以轻松地生成一个代码,该代码对于 2 个过滤器的情况是最佳的,并且在其他数量的过滤器的情况下仍然可以工作(想象一个简单的构建器):
return (filters.length) == 2 ? new FilterChain2(filters) : new FilterChain1(filters)。 JITC 可以这样做吗?如果不能,为什么? - JITC 能否检测到“filters.length==2”是最常见的情况,并在预热后生成最适合这种情况的代码?这应该几乎与手动展开的版本一样最佳。
- JITC 能否检测到某个特定实例被非常频繁地使用,然后为该特定实例生成一个代码(它知道过滤器的数量始终为 2 )?
更新: 得到的答案是 JITC 仅在班级级别上有效。好的,知道了。
理想情况下,我希望得到对 JITC 工作原理有深入了解的人的回答。
基准运行详情:
- 在最新版本的 Java 8 OpenJDK 和 Oracle HotSpot 上试过,结果差不多
- 使用的 Java 标志:-Xmx4g -Xms4g -server -Xbatch -XX:CICompilerCount=2(没有花哨的标志也得到了类似的结果)
- 顺便说一句,如果我简单地在一个循环中运行数十亿次(不是通过 JMH),我会得到类似的运行时间比率,即第二个 sn-p 显然总是更快
典型的基准测试输出:
基准 (filterIndex) 模式 Cnt 分数 错误单位
LoopUnrollingBenchmark.runBenchmark 0 平均 400 44.202 ± 0.224 ns/操作
LoopUnrollingBenchmark.runBenchmark 1 平均 400 38.347 ± 0.063 纳秒/操作
(第一行对应第一个sn-p,第二行-对应第二个。
完整的基准代码:
public class LoopUnrollingBenchmark {
@State(Scope.Benchmark)
public static class BenchmarkData {
public Filter[] filters;
@Param({"0", "1"})
public int filterIndex;
public int num;
@Setup(Level.Invocation) //similar ratio with Level.TRIAL
public void setUp() {
filters = new Filter[]{new FilterChain1(), new FilterChain2()};
num = new Random().nextInt();
}
}
@Benchmark
@Fork(warmups = 5, value = 20)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public int runBenchmark(BenchmarkData data) {
Filter filter = data.filters[data.filterIndex];
int sum = 0;
int num = data.num;
if (filter.isOK(num)) {
++sum;
}
if (filter.isOK(num + 1)) {
++sum;
}
if (filter.isOK(num - 1)) {
++sum;
}
if (filter.isOK(num * 2)) {
++sum;
}
if (filter.isOK(num * 3)) {
++sum;
}
if (filter.isOK(num * 5)) {
++sum;
}
return sum;
}
interface Filter {
boolean isOK(int i);
}
static class Filter1 implements Filter {
@Override
public boolean isOK(int i) {
return i % 3 == 1;
}
}
static class Filter2 implements Filter {
@Override
public boolean isOK(int i) {
return i % 7 == 3;
}
}
static class FilterChain1 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
for (int j = 0; j < filters.length; ++j) {
if (!filters[j].isOK(i)) {
return false;
}
}
return true;
}
}
static class FilterChain2 implements Filter {
final Filter[] filters = createLeafFilters();
@Override
public boolean isOK(int i) {
return filters[0].isOK(i) && filters[1].isOK(i);
}
}
private static Filter[] createLeafFilters() {
Filter[] filters = new Filter[2];
filters[0] = new Filter1();
filters[1] = new Filter2();
return filters;
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
【问题讨论】:
-
编译器不能保证数组的长度是2。我不确定它是否会展开它,即使它可以。
-
@Setup(Level.Invocation):不确定是否有帮助(请参阅 javadoc)。 -
由于无法保证数组的长度始终为 2,因此这两种方法的作用不同。 JIT 怎么会允许自己将第一个变成第二个?
-
@Andreas 我建议您回答这个问题,但请详细说明为什么 JIT 在这种情况下无法展开,与另一个类似的情况相比,它可以
-
@Alexander JIT 可以看到创建后数组长度不能改变,因为字段是
final,但是JIT没有看到所有实例 类将获得一个长度为 2 的数组。要看到这一点,它必须深入研究createLeafFilters()方法并深入分析代码以了解该数组始终是 2 长。为什么您认为 JIT 优化器会深入到您的代码中?
标签: java performance optimization jit