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

sik*_*kan 74 java performance compiler-optimization pre-increment post-increment

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

final int n = Integer.MAX_VALUE;
int i = 0;
while (++i < n) {
}
Run Code Online (Sandbox Code Playgroud)

比以下情况快至少2个数量级(~10 ms vs.~5000 ms):

final int n = Integer.MAX_VALUE;
int i = 0;
while (i++ < n) {
}
Run Code Online (Sandbox Code Playgroud)

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

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

iinc
iload
ldc
if_icmplt
Run Code Online (Sandbox Code Playgroud)

而对于较慢的版本:

iload
iinc
ldc
if_icmplt
Run Code Online (Sandbox Code Playgroud)

因此++i < n,它首先将局部变量递增i1,然后将其推入操作数堆栈,同时i++ < n以相反的顺序执行这两个步骤.但这似乎并不能解释为什么前者要快得多.后一种情况是否涉及临时副本?或者是否应该负责性能差异的字节码(VM实现,硬件等)以外的东西?

我已经阅读了一些关于++ii++(但并非详尽无遗)的其他讨论,但未找到任何特定于Java的答案,并且与价值比较中涉及++ii++涉及的情况直接相关.

Mar*_*o13 118

正如其他人所指出的那样,这项测试在很多方面存在缺陷.

您没有告诉我们您是如何进行此测试的.但是,我试图实现这样的"天真"测试(没有冒犯):

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) {}
    }
}
Run Code Online (Sandbox Code Playgroud)

使用默认设置运行时,似乎有一点不同.但是当你用旗帜运行时,基准的真正缺陷就变得很明显了-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
Run Code Online (Sandbox Code Playgroud)

显然,预增量版本已经完全优化了.原因很简单:没有使用结果.无论循环是否执行都没关系,因此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    
Run Code Online (Sandbox Code Playgroud)

后增量版本导致此代码:

[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    
Run Code Online (Sandbox Code Playgroud)

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


编辑:有趣的是,从改变环路的上限时Integer.MAX_VALUEInteger.MAX_VALUE-1,那么这两个版本被优化掉,并且需要"零"的时间.不知何故,这个限制(仍然0x7fffffff在程序集中出现)阻止了优化.据推测,这与比较被映射到(被烧毁的!)cmp指令有关,但除此之外我无法给出深刻的理由.JIT以神秘的方式运作......

  • 其实,这是它进入了我的脑海里的第一件事:当`而(我++ <Integer.MAX_VALUE的)`退出循环,溢出已经发生`i`.当溢出可能发生时,证明代码转换的正确性要困难得多,毕竟溢出的循环不常见,所以为什么热点还要优化它们... (26认同)
  • @RLH我在http://stackoverflow.com/questions/25326377/jit-not-optimizing-loop-that-involves-integer-max-value上发布了一个后续问题 (5认同)
  • 我不是一个java人,但我确实对编译器的机制感兴趣.如果您(或任何人)碰巧在另一个帖子上询问您的后续问题,请发布链接.谢谢! (2认同)

Smi*_*_61 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;
Run Code Online (Sandbox Code Playgroud)

在您的情况下,JVM似乎不会优化增量,因为您在表达式中使用结果.另一方面,JVM可以像这样优化循环.

for( int i = 0; i < Integer.MAX_VALUE; i++ ) {}
Run Code Online (Sandbox Code Playgroud)

这是因为从未使用过i ++的结果.在这样的循环中,您应该能够使用++ i和i ++,其性能与使用++ i时相同.

  • 正如OP所提到的,两个版本都产生相同数量的字节码指令.你在那里谈论的开销在哪里?你谈到的JVM优化对于`++ i`版本是可能的,而对另一个版本是不可能的? (10认同)

Eug*_*ene 18

编辑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
Run Code Online (Sandbox Code Playgroud)

这几乎没有区别

你应该问自己的第一个问题是你如何测试和测量它.这是微基准测试,在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;
    }
}
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

分数字段是您感兴趣的字段.