为什么“while(i ++ <n){}”明显慢于“while(++ i <n){}”
显然,在我的Windows 8笔记本电脑上安装了HotSpot JDK 1.7.0_45(所有编译器/虚拟机选项设置为默认),下面的循环
final int n = Integer.MAX_VALUE; int i = 0; while (++i < n) { }
至less快两个数量级(〜10ms对比~5000ms):
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
,它首先将局部variablesi
递增1,然后将其推到操作数栈上,而i++ < n
以相反的顺序执行这2个步骤。 但这似乎并不能解释为什么前者要快得多。 在后一种情况下是否有临时副本? 还是超越字节码(虚拟机实施,硬件等),应该是性能差异负责?
我已经阅读了关于++i
和i++
其他讨论(虽然并不详尽),但是没有find任何与Java特定相关的答案,并且直接关系到++i
或i++
参与值比较的情况。
正如其他人所指出的那样,这个testing在很多方面都是有缺陷的。
你没有告诉我们你是如何做这个testing的。 不过,我试图实施一个“天真”的testing(没有冒犯),像这样:
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
标志运行时,基准的真正缺陷会变-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
我不完全清楚为什么它似乎不删除后增量版本。 (事实上,我认为这是一个单独的问题)。 但至less,这解释了为什么你可能会看到与“数量级”的差异…
编辑:有趣的是,将循环的上限从Integer.MAX_VALUE
更改为Integer.MAX_VALUE-1
, 两个版本都被优化了,并要求“零”时间。 不知何故,这个限制(在程序集中仍然显示为0x7fffffff
)阻止了优化。 据推测,这与比较被映射到一个(单曲!) cmp
指令有关,但是除此之外,我不能给出深刻的理由。 JIT以神秘的方式工作…
++ i和i ++之间的区别在于++我有效地增加了variables,并且返回了新的值。 另一方面,i ++有效地创build一个临时variables来保存当前值,然后递增variables返回临时variables的值。 这是额外的开销来自哪里。
// 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优化,因为你在expression式中使用了结果。 另一方面,JVM可以像这样优化一个循环。
for( int i = 0; i < Integer.MAX_VALUE; i++ ) {}
这是因为我从来没有使用i ++的结果。 在这样的循环中,你应该可以像使用++ i一样使用++ i和i ++的性能。
编辑2
你应该看看这里:
编辑我越想它,我意识到这个testing是不知何故错误,循环会得到认真优化的JVM。
我认为你应该放弃@Param
,让n=2
。
这样你就可以testing一下自己的performance。 我在这种情况下得到的结果是:
omtWhileTest.testFirst avgt 5 0.787 0.086 ns/op omtWhileTest.testSecond avgt 5 0.782 0.087 ns/op
这几乎没有区别
你应该问自己的第一个问题是你如何testing和测量 。 这是微观基准testing,在Java中这是一门艺术,几乎总是一个简单的用户(像我)会得到错误的结果。 你应该依靠基准testing和非常好的工具。 我用JMH来testing这个:
@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 omtWhileTest.testFirst 100 avgt 5 1.271 0.096 ns/op omtWhileTest.testFirst 10000 avgt 5 1.319 0.125 ns/op omtWhileTest.testFirst 100000 avgt 5 1.327 0.241 ns/op omtWhileTest.testFirst 1000000 avgt 5 1.311 0.136 ns/op omtWhileTest.testSecond 100 avgt 5 1.450 0.525 ns/op omtWhileTest.testSecond 10000 avgt 5 1.563 0.479 ns/op omtWhileTest.testSecond 100000 avgt 5 1.418 0.428 ns/op omtWhileTest.testSecond 1000000 avgt 5 1.344 0.120 ns/op
分数字段是您感兴趣的那个。
可能这个testing还不足以得出结论,但是我会说如果是这样的话,JVM可以通过将i ++改成++ i来优化这个expression式,因为在这个循环中永远不会使用i ++(pre值)的存储值。
我build议你应该(只要有可能) 总是使用++c
而不是c++
因为前者永远不会变慢,因为在概念上,为了返回前一个值,必须在后一种情况下采用c
的深层副本。
实际上,许多优化器会优化掉不必要的深层副本,但是如果使用expression式值,他们不会轻易做到这一点。 你就是这样做的。
许多民众不同意,他们认为这是一个微观优化。