为什么“ while (i + + < n){}”明显慢于“ while (+ + i < n){}”

显然,在我的带有 HotSpot JDK 1.7.0 _ 45(所有编译器/VM 选项都设置为默认)的 Windows 8笔记本电脑上,下面的循环

final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {
}

至少比下列数量级快2倍(约10毫秒比约5000毫秒) :

final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {
}

我在编写用于评估另一个不相关性能问题的循环时碰巧注意到了这个问题。++i < ni++ < n之间的差异足以显著影响检测结果。

如果我们查看字节码,更快版本的循环体是:

iinc
iload
ldc
if_icmplt

慢一点的版本是:

iload
iinc
ldc
if_icmplt

因此,对于 ++i < n,它首先将局部变量 i增加1,然后将其推送到操作数堆栈上,而 i++ < n按相反的顺序执行这两个步骤。但这似乎无法解释为什么前者要快得多。在后一种情况下是否有临时副本?或者是字节码之外的东西(VM 实现、硬件等)应该对性能差异负责?

我已经阅读了一些关于 ++ii++的讨论(尽管不是详尽的) ,但是没有找到任何特定于 Java 的答案,也没有找到任何与 ++ii++参与值比较的情况直接相关的答案。

10015 次浏览

+ + i 和 i + + 之间的区别在于,+ + i 有效地递增了变量,并且“返回”了新值。另一方面,i + + 有效地创建了一个临时变量来保存 i 中的当前值,然后递增变量“返回”临时变量的值。这就是额外开销的来源。

// 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 相同的性能。

我建议你应该(只要有可能)使用 一直都是而不是 c++,因为前者将 永远不会比较慢,因为从概念上讲,在后一种情况下,为了返回先前的值,必须采用 c的深拷贝。

事实上,许多优化器会优化掉不必要的深度副本,但是如果你使用了表达式值,他们就不能轻易做到这一点。你的情况就是这样。

不过,许多人不同意这种观点: 他们认为这是一种微观优化。

编辑2

你应该看看这里:

Http://hg.openjdk.java.net/code-tools/jmh/file/f90aef7f1d2c/jmh-samples/src/main/java/org/openjdk/jmh/samples/jmhsample_11_loops.java

剪辑 我越想越觉得这个测试是错误的,循环会被 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

Score 字段是您感兴趣的字段。

正如其他人指出的那样,该测试在许多方面存在缺陷。

你没有告诉我们你做了这个测试。然而,我试图实现这样一个“幼稚”的测试(无意冒犯) :

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} &apos;runPreIncrement&apos; &apos;()V&apos; in &apos;PrePostIncrement&apos;
#           [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} &apos;runPostIncrement&apos; &apos;()V&apos; in &apos;PrePostIncrement&apos;
#           [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

我不完全清楚为什么看起来 没有移除了后增量版本。(事实上,我认为这是一个单独的问题)。但至少,这解释了为什么你可能会看到与“数量级”的差异... ..。


编辑: 有趣的是,当改变循环的上限从 Integer.MAX_VALUEInteger.MAX_VALUE-1,然后 都有版本是优化了,需要“零”的时间。不知何故,这个限制(在程序集中仍然显示为 0x7fffffff)阻止了优化。据推测,这与映射到(singed!)的比较有关cmp指令,但除此之外我不能给出一个深刻的理由。JIT 的工作方式很神秘。

可能这个测试还不足以得出结论,但是如果是这样的话,JVM 可以通过将 i + + 更改为 + + i 来优化这个表达式,因为 i + + (pre 值)的存储值从未在这个循环中使用过。