我遇到过一个案例,我认为 JIT 应该可以很容易地进行时间优化,但事实并非如此。
我已将问题简化为一个最小的示例:
考虑一个类 IntArrayWrapper
:
class IntArrayWrapper {
private int[] data = new int[100000];
public void setInteger(int i, Integer x) { data[i] = x; }
public void setInt (int i, int x) { data[i] = x; }
}
这两种方法之间的唯一区别是 x
是 Integer
(盒装)或 int
(原始)。
我已经编写了一些 JMH 基准来衡量这两种方法的性能:
@Benchmark
public void bmarkSetIntConst() {
final IntArrayWrapper w = new IntArrayWrapper();
for (int i = 0; i < 100000; i++) {
w.setInt(i, 100);
}
}
@Benchmark
public void bmarkSetIntStair() {
final IntArrayWrapper w = new IntArrayWrapper();
for (int i = 0; i < 100000; i++) {
w.setInt(i, i);
}
}
// omitted: bmarkSetIntegerConst and bmarkSetIntStair that use .setInteger(..)
预期结果
我希望看到的是:
setIntegerConst
等于setIntConst
。 这是真的。setIntegerStair
等于setIntStair
。 这不是真的。
我认为的原因是我认为 JIT 应该内联 setInteger
调用,并意识到有一个自动装箱操作(来自调用),紧接着是一个拆箱操作(来自数组分配),因此能够删除装箱/拆箱。
这似乎是的情况。
一些观察
- const 操作执行得同样好,我认为这是因为缓存了硬编码整数。
- 奇怪的是
setIntStair
和setIntConst
有不同的性能,我感觉 JIT 可能会在这里生成 SIMD 代码,但我将不胜感激任何见解。
结果
这些是结果,整个代码在这里:https://gist.github.com/kaeluka/fe1210074038424c30db7a52ac5c2d7b
Benchmark Mode Cnt Score Error Units
MyBenchmark.bmarkSetIntConst thrpt 20 15717.814 ± 362.137 ops/s
MyBenchmark.bmarkSetIntegerConst thrpt 20 15814.296 ± 657.945 ops/s
MyBenchmark.bmarkSetIntStair thrpt 20 11941.879 ± 200.335 ops/s
MyBenchmark.bmarkSetIntegerStair thrpt 20 2981.398 ± 48.806 ops/s
MyBenchmark.bmarkSetIntSawtooth thrpt 20 11072.882 ± 234.686 ops/s
MyBenchmark.bmarkSetIntegerSawtooth thrpt 20 11105.272 ± 156.496 ops/s
问题
- 为什么 JIT 不能省略装箱?
- 有没有办法在不更改
setInteger
接口(interface)以获取int
的情况下解决此问题? (我的原始代码使用泛型,所以int
不是一个选项,除非我想复制很多代码)。
编辑
添加了 bmarkSetIntegerSawtooth
和 bmarkSetIntSawtooth
的结果,将值设置为 i % 128
以衡量对象池对 Integer 的影响
s.
最佳答案
Why is the JIT not able to elide boxing?
我猜 JIT 并不专门针对装箱操作,而是依靠定期的逃逸分析来消除不必要的装箱。逃逸分析对数据流相当挑剔,我怀疑问题是您的一些 装箱操作命中了Integer
缓存。潜在地从缓存中提取值可能是阻止消除装箱的原因。
我以两种方式修改了您的测试,并测量了每种方式的结果。结果似乎证实了我的假设。
我首先尝试重写您的基准测试以使用装箱的
double
值而不是int
值,因为double
装箱不涉及任何缓存。然后我回到了基于
int
的基准测试,但是修改了你的循环以从i = 128
开始,这样你的装箱操作就不会命中缓存。
在这两种情况下,性能差距都在误差范围内。
为了确认,我启用了 -XX:+PrintAssembly
来查看我修改后的基准测试是如何编译的。对于每对基准测试,装箱和原始变体具有几乎相同的指令序列。只有细微差别,例如,一对指令翻转了。看起来拳击确实被优化掉了。
解决方法:由于绕过缓存似乎可以避免这个问题,而且没有办法强制一个空的整数缓存,一个解决方法是用 new Integer(i) 替换隐式装箱
。但是请注意,如果逃逸分析没有替换分配(由于达到各种编译器阈值之一),那么您的性能实际上可能降低。
修改后的基准:
class IntArrayWrapper {
private int[] data = new int[100000];
void setBoxed(int i, Integer x) { data[i] = x; }
void setUnboxed(int i, int x) { data[i] = x; }
}
class DoubleArrayWrapper {
private double[] data = new double[100000];
void setBoxed(int i, Double x) { data[i] = x; }
void setUnboxed(int i, double x) { data[i] = x; }
}
@State(Scope.Benchmark)
public class BoxingBenchmarks {
@Benchmark
public void intBoxed() {
final IntArrayWrapper w = new IntArrayWrapper();
for (int i = 128; i < 100000; i++) w.setBoxed(i, i);
}
@Benchmark
public void intUnboxed() {
final IntArrayWrapper w = new IntArrayWrapper();
for (int i = 128; i < 100000; i++) w.setUnboxed(i, i);
}
@Benchmark
public void doubleBoxed() {
final DoubleArrayWrapper w = new DoubleArrayWrapper();
for (int i = 0; i < 100000; i++) w.setBoxed(i, (double) i);
}
@Benchmark
public void doubleUnboxed() {
final DoubleArrayWrapper w = new DoubleArrayWrapper();
for (int i = 0; i < 100000; i++) w.setUnboxed(i, (double) i);
}
}
结果:
Benchmark Mode Cnt Score Error Units
BoxingBenchmarks.doubleBoxed thrpt 5 6513.760 ± 1075.605 ops/s
BoxingBenchmarks.doubleUnboxed thrpt 5 6883.235 ± 414.803 ops/s
BoxingBenchmarks.intBoxed thrpt 5 10902.200 ± 315.437 ops/s
BoxingBenchmarks.intUnboxed thrpt 5 11148.648 ± 935.877 ops/s
关于java - 奇数表现(盒装整数),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50046221/