我使用java
for循环进行了一些运行时测试,并发现了一种奇怪的行为。对于我的代码,我需要原始类型(例如int,double等)的包装对象来模拟io和输出参数,但这不是重点。只是看我的代码。具有字段访问权限的对象如何比原始类型更快?
for
优先类型的循环:
public static void main(String[] args) {
double max = 1000;
for (int j = 1; j < 8; j++) {
double i;
max = max * 10;
long start = System.nanoTime();
for (i = 0; i < max; i++) {
}
long end = System.nanoTime();
long microseconds = (end - start) / 1000;
System.out.println("MicroTime primitive(max: ="+max + "): " + microseconds);
}
}
结果:
MicroTime原语(最大值:= 10000.0):110
MicroTime原语(最大值:= 100000.0):1081
MicroTime原语(最大值:=
1000000.0 ):2450 MicroTime原语(最大值:= 1.0E7):28248
MicroTime原语(最大值:= 1.0E8) :276205
MicroTime原语(最大值:= 1.0E9):2729824
MicroTime原语(最大值:= 1.0E10):27547009
for
简单类型(包装对象)循环:
public static void main(String[] args) {
HDouble max = new HDouble();
max.value = 1000;
for (int j = 1; j < 8; j++) {
HDouble i = new HDouble();
max.value = max.value*10;
long start = System.nanoTime();
for (i.value = 0; i.value <max.value; i.value++) {
}
long end = System.nanoTime();
long microseconds = (end - start) / 1000;
System.out.println("MicroTime wrapper(max: ="+max.value + "): " + microseconds);
}
}
结果:
MicroTime包装器(最大值:= 10000.0):157
MicroTime包装器(最大值:= 100000.0):1561
MicroTime包装器(最大值:=
1000000.0 ):3174 MicroTime包装器(最大值:= 1.0E7):15630
MicroTime包装器(最大值:= 1.0E8) :155471
MicroTime包装器(最大值:= 1.0E9):1520967
MicroTime包装器(最大值:= 1.0E10):15373311
迭代次数越多,第二个代码就会越快。但为什么?我知道java-compiler和jvm正在优化我的代码,但我从未想到原始类型会比具有字段访问权限的对象慢。
有人对此有合理的解释吗?
编辑:HDouble类:
public class HDouble {
public double value;
public HDouble() {
}
public HDouble(double value) {
this.value = value;
}
@Override
public String toString() {
return String.valueOf(value);
}
}
我还用代码测试了我的循环。例如,我计算总和->相同的行为(相差不那么大,但我认为原始算法必须快得多吗?)。首先,我认为计算所需的时间如此之久,现场访问几乎没有区别。
包装器循环:
for (i.value = 0; i.value <max.value; i.value++) {
sum.value = sum.value + i.value;
}
结果:
MicroTime包装器(最大值:= 10000.0):243
MicroTime包装器(最大值:= 100000.0):2805
MicroTime包装器(最大值:=
1000000.0 ):3409 MicroTime包装器(最大值:= 1.0E7):28104
MicroTime包装器(最大值:= 1.0E8) :278432
MicroTime包装器(最大值:= 1.0E9):2678322
MicroTime包装器(最大值:= 1.0E10):26665540
原始for循环:
for (i = 0; i < max; i++) {
sum = sum + i;
}
结果:
MicroTime原语(最大值:= 10000.0):149
MicroTime原语(最大值:= 100000.0):1996
MicroTime原语(最大值:=
1000000.0 ):2289 MicroTime原语(最大值:= 1.0E7):27085
MicroTime原语(最大值:= 1.0E8) :
279939 MicroTime原语(最大值:= 1.0E9):2759133
MicroTime原语(最大值:= 1.0E10):27369724
容易被手工制作的微基准所迷惑-您永远不知道它们的 实际
测量值。这就是为什么有诸如JMH之类的特殊工具的原因。但是,让我们分析一下原始的手工基准会发生什么:
static class HDouble {
double value;
}
public static void main(String[] args) {
primitive();
wrapper();
}
public static void primitive() {
long start = System.nanoTime();
for (double d = 0; d < 1000000000; d++) {
}
long end = System.nanoTime();
System.out.printf("Primitive: %.3f s\n", (end - start) / 1e9);
}
public static void wrapper() {
HDouble d = new HDouble();
long start = System.nanoTime();
for (d.value = 0; d.value < 1000000000; d.value++) {
}
long end = System.nanoTime();
System.out.printf("Wrapper: %.3f s\n", (end - start) / 1e9);
}
结果有点类似于您的结果:
Primitive: 3.618 s
Wrapper: 1.380 s
现在重复测试几次:
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
primitive();
wrapper();
}
}
它变得更加有趣:
Primitive: 3.661 s
Wrapper: 1.382 s
Primitive: 3.461 s
Wrapper: 1.380 s
Primitive: 1.376 s <-- starting from 3rd iteration
Wrapper: 1.381 s <-- the timings become equal
Primitive: 1.371 s
Wrapper: 1.372 s
Primitive: 1.379 s
Wrapper: 1.378 s
看来这两种方法都最终得到了优化。再次运行它,现在记录JIT编译器活动: -XX:-TieredCompilation -XX:CompileOnly=Test -XX:+PrintCompilation
136 1 % Test::primitive @ 6 (53 bytes)
3725 1 % Test::primitive @ -2 (53 bytes) made not entrant
Primitive: 3.589 s
3748 2 % Test::wrapper @ 17 (73 bytes)
5122 2 % Test::wrapper @ -2 (73 bytes) made not entrant
Wrapper: 1.374 s
5122 3 Test::primitive (53 bytes)
5124 4 % Test::primitive @ 6 (53 bytes)
Primitive: 3.421 s
8544 5 Test::wrapper (73 bytes)
8547 6 % Test::wrapper @ 17 (73 bytes)
Wrapper: 1.378 s
Primitive: 1.372 s
Wrapper: 1.375 s
Primitive: 1.378 s
Wrapper: 1.373 s
Primitive: 1.375 s
Wrapper: 1.378 s
注意%
在第一次迭代时登录编译日志。这意味着这些方法是在OSR
(堆栈替换)模式下编译的。在第二次迭代中,方法以正常模式重新编译。从那时起,从第三次迭代开始,原语和包装器之间的执行速度就没有区别。
您实际测量的是OSR存根的性能。它通常与应用程序的实际性能无关,因此您不必在意它。
但是问题仍然存在,为什么包装的OSR存根比原始变量的编译好?为了找出答案,我们需要深入研究生成的汇编代码:
-XX:CompileOnly=Test -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
我将忽略所有不相关的代码,仅保留编译循环。
原始:
0x00000000023e90d0: vmovsd 0x28(%rsp),%xmm1 <-- load double from the stack
0x00000000023e90d6: vaddsd -0x7e(%rip),%xmm1,%xmm1
0x00000000023e90de: test %eax,-0x21f90e4(%rip)
0x00000000023e90e4: vmovsd %xmm1,0x28(%rsp) <-- store to the stack
0x00000000023e90ea: vucomisd 0x28(%rsp),%xmm0 <-- compare with the stack value
0x00000000023e90f0: ja 0x00000000023e90d0
包装器:
0x00000000023ebe90: vaddsd -0x78(%rip),%xmm0,%xmm0
0x00000000023ebe98: vmovsd %xmm0,0x10(%rbx) <-- store to the object field
0x00000000023ebe9d: test %eax,-0x21fbea3(%rip)
0x00000000023ebea3: vucomisd %xmm0,%xmm1 <-- compare registers
0x00000000023ebea7: ja 0x00000000023ebe90
如您所见,“原始”情况会产生许多负载并存储到堆栈位置,而“包装器”主要执行寄存器内操作。为什么OSR存根引用堆栈是完全可以理解的:在解释模式下,局部变量存储在堆栈中,并使OSR存根与此解释帧兼容。在“包装器”情况下,该值存储在堆中,并且对该对象的引用已缓存在寄存器中。
问题内容: 通过一些旧的公司代码,我遇到了一个如下所示的for循环: 我尝试了Google,但找不到任何答案。我是在编程课上睡着了还是这是一个不寻常的循环? 问题答案: 一个for在Java循环结构如下- for (initialization statement; condition check; update) loop body; 如你所见,这里有四个语句- 初始化语句:第一次进入循环时,该
有时候你会遇到循环,或者递归函数,它们会花费很长的执行时间,可能是你的产品的瓶颈。在你尝试使循环变得快一点之前,花几分钟考虑是否有可能把它整个移除掉,有没有一个不同的算法?你可以在计算时做一些其他的事情吗?如果你不能找到一个方法去绕开它,你可以优化这个循环了。这是很简单的,move stuff out。最后,这不仅需要智慧而且需要理解每一种语句和表达式的开销。这里是一些建议: 删除浮点运算操作。
我有一个任务来优化for循环,以便编译器编译运行更快的代码。目标是使代码在 5 秒或更短的时间内运行,原始运行时间约为 23 秒。原始代码如下所示: 我的第一个想法是在内部for循环上进行循环展开,使它降到5.7秒,循环看起来像这样: 在每个循环的阵列中将其取出12个点后,性能不再增加,所以我的下一个想法是尝试引入一些并行性,所以我做了这个: 这实际上最终减慢了代码的速度,并且每个附加变量再次减慢
问题内容: 我在学校被告知,修改a的index变量是一种不好的做法: 范例: 有论据认为, 某些编译器优化可以优化循环, 而无需重新计算索引并限制每个循环。 我进行了一些测试,似乎默认情况下每次都会重新计算索引和绑定。 我想知道是否有可能在中激活这种功能? 例如,优化这种循环: 无需写: 这只是一个例子,我很想尝试一下改进。 编辑 根据Peter Lawrey的回答, 为什么在这个简单的示例中JV
问题内容: 我听说Java支持“循环取消切换”,因此我只是在JMH中对其进行了测试。 我认为在JIT之后它们将完全相同。为什么是这样? 检测结果 测试环境 问题答案: JMH禁用方法的内联。非内联方法是JVM的黑匣子- 编译器不知道该方法是否会修改字段,引发异常,将其注册为垃圾等。JIT编译器无法在此类方法调用中应用许多优化。(想象一下,黑盒方法使用反射来修改字段,因此循环取消切换将变得无效)。
问题内容: 在写另一个问题的答案时,我注意到用于JIT优化的奇怪边框。 以下程序 不是 “ Microbenchmark”, 也不 旨在可靠地衡量执行时间(如对另一个问题的回答所指出)。它仅用作MCVE来重现此问题: 它基本上运行相同的循环,其中将限制一次设置为,将一次设置为。 当在Win7 / 64上使用JDK 1.7.0_21和 计时结果如下: 显然,对于JIT 的情况,JIT可以完成预期的工