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

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

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

因此,对于 ,它首先将局部变量递增 1,然后将其推送到操作数堆栈上,同时以相反的顺序执行这 2 个步骤。但这似乎并不能解释为什么前者要快得多。后一种情况下是否涉及任何临时副本?还是字节码(VM实现,硬件等)之外的东西应该导致性能差异?++i < nii++ < n

我读过一些关于和的其他讨论(虽然不是详尽无遗的),但没有找到任何特定于Java的答案,并且与价值比较中涉及或涉及的情况直接相关。++ii++++ii++


答案 1

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

您没有确切告诉我们您是如何进行此测试的。但是,我试图实现一个“幼稚”测试(没有冒犯),如下所示:

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    

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


编辑:有趣的是,当将循环的上限从 更改为 时,两个版本都被优化并需要“零”时间。不知何故,此限制(仍然显示在程序集中)阻止了优化。据推测,这与将比较映射到(唱! 指导,但除此之外,我无法给出深刻的理由。JIT以神秘的方式工作...Integer.MAX_VALUEInteger.MAX_VALUE-10x7fffffffcmp


答案 2

++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++。


推荐