显然在我的Windows 8笔记本电脑上使用HotSpot JDK 1.7.0_45(所有编译器/ VM选项都设置为默认值),以下循环
final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {
}
比:
快至少2个数量级(~10 ms与~5000 ms)final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {
}
在写一个循环来评估另一个不相关的性能问题时,我碰巧注意到了这个问题。 ++i < n
和i++ < n
之间的差异足以显着影响结果。
如果我们查看字节码,更快版本的循环体是:
iinc
iload
ldc
if_icmplt
对于较慢的版本:
iload
iinc
ldc
if_icmplt
因此,对于++i < n
,它首先将局部变量i
递增1,然后将其推入操作数堆栈,而i++ < n
以相反的顺序执行这两个步骤。但这似乎并不能解释为什么前者要快得多。后一种情况是否涉及临时副本?或者它应该是字节码(VM实现,硬件等)之外应该对性能差异负责的东西吗?
我已经阅读了一些关于++i
和i++
的其他讨论(尽管不是详尽无遗),但没有找到任何特定于Java的答案,而且与{{1}的情况直接相关或} ++i
参与值比较。
答案 0 :(得分:118)
正如其他人所指出的,这项测试在很多方面存在缺陷。
您没有完全告诉我们 您是如何进行此项测试的。但是,我试图实现一个天真的&#34;像这样测试(没有冒犯):
class PrePostIncrement
{
public static void main(String args[])
{
for (int j=0; j<3; j++)
{
for (int i=0; i<5; i++)
{
long before = System.nanoTime();
runPreIncrement();
long after = System.nanoTime();
System.out.println("pre : "+(after-before)/1e6);
}
for (int i=0; i<5; i++)
{
long before = System.nanoTime();
runPostIncrement();
long after = System.nanoTime();
System.out.println("post : "+(after-before)/1e6);
}
}
}
private static void runPreIncrement()
{
final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {}
}
private static void runPostIncrement()
{
final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {}
}
}
使用默认设置运行时,似乎有一点不同。但是当您使用-server
标志运行时,基准的真实缺陷变得明显。在我的情况下的结果是类似
...
pre : 6.96E-4
pre : 6.96E-4
pre : 0.001044
pre : 3.48E-4
pre : 3.48E-4
post : 1279.734543
post : 1295.989086
post : 1284.654267
post : 1282.349093
post : 1275.204583
显然,预增量版本已经完全优化。原因很简单:结果没有使用。无论循环是否执行都没关系,因此JIT只是将其删除。
通过查看热点反汇编来确认:预增量版本会产生以下代码:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x0000000055060500} 'runPreIncrement' '()V' in 'PrePostIncrement'
# [sp+0x20] (sp of caller)
0x000000000286fd80: sub $0x18,%rsp
0x000000000286fd87: mov %rbp,0x10(%rsp) ;*synchronization entry
; - PrePostIncrement::runPreIncrement@-1 (line 28)
0x000000000286fd8c: add $0x10,%rsp
0x000000000286fd90: pop %rbp
0x000000000286fd91: test %eax,-0x243fd97(%rip) # 0x0000000000430000
; {poll_return}
0x000000000286fd97: retq
0x000000000286fd98: hlt
0x000000000286fd99: hlt
0x000000000286fd9a: hlt
0x000000000286fd9b: hlt
0x000000000286fd9c: hlt
0x000000000286fd9d: hlt
0x000000000286fd9e: hlt
0x000000000286fd9f: hlt
增量后版本会产生以下代码:
[Entry Point]
[Verified Entry Point]
[Constants]
# {method} {0x00000000550605b8} 'runPostIncrement' '()V' in 'PrePostIncrement'
# [sp+0x20] (sp of caller)
0x000000000286d0c0: sub $0x18,%rsp
0x000000000286d0c7: mov %rbp,0x10(%rsp) ;*synchronization entry
; - PrePostIncrement::runPostIncrement@-1 (line 35)
0x000000000286d0cc: mov $0x1,%r11d
0x000000000286d0d2: jmp 0x000000000286d0e3
0x000000000286d0d4: nopl 0x0(%rax,%rax,1)
0x000000000286d0dc: data32 data32 xchg %ax,%ax
0x000000000286d0e0: inc %r11d ; OopMap{off=35}
;*goto
; - PrePostIncrement::runPostIncrement@11 (line 36)
0x000000000286d0e3: test %eax,-0x243d0e9(%rip) # 0x0000000000430000
;*goto
; - PrePostIncrement::runPostIncrement@11 (line 36)
; {poll}
0x000000000286d0e9: cmp $0x7fffffff,%r11d
0x000000000286d0f0: jl 0x000000000286d0e0 ;*if_icmpge
; - PrePostIncrement::runPostIncrement@8 (line 36)
0x000000000286d0f2: add $0x10,%rsp
0x000000000286d0f6: pop %rbp
0x000000000286d0f7: test %eax,-0x243d0fd(%rip) # 0x0000000000430000
; {poll_return}
0x000000000286d0fd: retq
0x000000000286d0fe: hlt
0x000000000286d0ff: hlt
我并不完全清楚为什么它似乎不删除后增量版本。 (事实上,我认为这是一个单独的问题)。但至少,这就解释了为什么你可能会看到与#34;数量级的差异&#34; ...
编辑:有趣的是,当将循环的上限从Integer.MAX_VALUE
更改为Integer.MAX_VALUE-1
时,两个版本都会被优化掉并且需要&#34;零&#34 ;时间。不知何故,此限制(在程序集中仍显示为0x7fffffff
)会阻止优化。据推测,这与映射到(烧尽的!)cmp
指令的比较有关,但除此之外我无法给出深刻的理由。 JIT以神秘的方式运作......
答案 1 :(得分:19)
++ i和i ++之间的区别在于++ i有效地增加变量并'返回'新值。另一方面,i ++有效地创建一个临时变量来保存i中的当前值,然后递增变量'return'temp变量的值。这是额外开销的来源。
// i++ evaluates to something like this
// Imagine though that somehow i was passed by reference
int temp = i;
i = i + 1;
return temp;
// ++i evaluates to
i = i + 1;
return i;
在您的情况下,JVM似乎不会优化增量,因为您在表达式中使用结果。另一方面,JVM可以像这样优化循环。
for( int i = 0; i < Integer.MAX_VALUE; i++ ) {}
这是因为从未使用过i ++的结果。在这样的循环中,您应该能够使用++ i和i ++,其性能与使用++ i时相同。
答案 2 :(得分:18)
编辑2
你应该看看这里:
修改强> 我越是想到它,我意识到这个测试在某种程度上是错误的,JVM会对循环进行认真优化。
我认为你应该放弃@Param
并让n=2
。
这样您就可以测试 while
本身的效果。我在这种情况下得到的结果:
o.m.t.WhileTest.testFirst avgt 5 0.787 0.086 ns/op
o.m.t.WhileTest.testSecond avgt 5 0.782 0.087 ns/op
几乎没有区别
您应该问自己的第一个问题是如何测试和衡量。 这是微基准测试,在Java中这是一门艺术,几乎总是一个简单的用户(像我一样)会得到错误的结果。你应该依靠基准测试和非常好的工具。我用JMH测试了这个:
@Measurement(iterations=5, time=1, timeUnit=TimeUnit.MILLISECONDS)
@Fork(1)
@Warmup(iterations=5, time=1, timeUnit=TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Benchmark)
public class WhileTest {
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(".*" + WhileTest.class.getSimpleName() + ".*")
.threads(1)
.build();
new Runner(opt).run();
}
@Param({"100", "10000", "100000", "1000000"})
private int n;
/*
@State(Scope.Benchmark)
public static class HOLDER_I {
int x;
}
*/
@Benchmark
public int testFirst(){
int i = 0;
while (++i < n) {
}
return i;
}
@Benchmark
public int testSecond(){
int i = 0;
while (i++ < n) {
}
return i;
}
}
JMH中更有经验的人可能会纠正这个结果(我真的希望如此!,因为我在JMH中并不是那么多用途),但结果表明差异非常小:
Benchmark (n) Mode Samples Score Score error Units
o.m.t.WhileTest.testFirst 100 avgt 5 1.271 0.096 ns/op
o.m.t.WhileTest.testFirst 10000 avgt 5 1.319 0.125 ns/op
o.m.t.WhileTest.testFirst 100000 avgt 5 1.327 0.241 ns/op
o.m.t.WhileTest.testFirst 1000000 avgt 5 1.311 0.136 ns/op
o.m.t.WhileTest.testSecond 100 avgt 5 1.450 0.525 ns/op
o.m.t.WhileTest.testSecond 10000 avgt 5 1.563 0.479 ns/op
o.m.t.WhileTest.testSecond 100000 avgt 5 1.418 0.428 ns/op
o.m.t.WhileTest.testSecond 1000000 avgt 5 1.344 0.120 ns/op
分数字段是您感兴趣的字段。
答案 3 :(得分:0)
可能这个测试不足以得出结论,但我想说如果是这种情况,JVM可以通过将i ++更改为++ i来优化此表达式,因为i ++(pre value)的存储值从未在此使用过循环。
答案 4 :(得分:-3)
我建议您(应尽可能)始终使用++c
而不是c++
因为前者永远不会更慢,因为从概念上讲,在后一种情况下,必须采用c
的深层副本才能返回之前的值。
事实上,许多优化工具会优化掉不必要的深层复制,但如果您正在使用表达式值,他们就不会轻易做到这一点。在你的情况下,你就是这么做的。
许多人不同意:他们认为这是一种微观优化。