为什么在 x64 Java 中 long 比 int 慢?

我在 Surface Pro 2 平板电脑上运行 Windows 8.1 x64 和 Java 7 更新 45 x64(未安装 32 位 Java)。

当 i 的类型为 long 时,下面的代码需要 1688ms,当 i 是 int 时,需要 109ms。为什么 long(64 位类型)在具有 64 位 JVM 的 64 位平台上比 int 慢一个数量级?

我唯一的推测是CPU添加64位整数比32位整数需要更长的时间,但这似乎不太可能。我怀疑Haswell没有使用涟漪携带加法器。

顺便说一句,我在Eclipse Kepler SR1中运行这个。

public class Main {

    private static long i = Integer.MAX_VALUE;

    public static void main(String[] args) {    
        System.out.println("Starting the loop");
        long startTime = System.currentTimeMillis();
        while(!decrementAndCheck()){
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
    }

    private static boolean decrementAndCheck() {
        return --i < 0;
    }

}

编辑:这是由VS 2013编译的等效C++代码的结果(如下),同一系统。长: 72265毫秒 国际: 74656毫秒这些结果处于调试 32 位模式。

在 64 位释放模式下:长:875 毫秒 长:906 毫秒 int:1047 毫秒

这表明我观察到的结果是JVM优化的怪异性,而不是CPU限制。

#include "stdafx.h"
#include "iostream"
#include "windows.h"
#include "limits.h"

long long i = INT_MAX;

using namespace std;


boolean decrementAndCheck() {
return --i < 0;
}


int _tmain(int argc, _TCHAR* argv[])
{


cout << "Starting the loop" << endl;

unsigned long startTime = GetTickCount64();
while (!decrementAndCheck()){
}
unsigned long endTime = GetTickCount64();

cout << "Finished the loop in " << (endTime - startTime) << "ms" << endl;



}

编辑:刚刚在Java 8 RTM中再次尝试,没有显着变化。


答案 1

我的JVM在使用s时对内部循环做了非常简单的事情:long

0x00007fdd859dbb80: test   %eax,0x5f7847a(%rip)  /* fun JVM hack */
0x00007fdd859dbb86: dec    %r11                  /* i-- */
0x00007fdd859dbb89: mov    %r11,0x258(%r10)      /* store i to memory */
0x00007fdd859dbb90: test   %r11,%r11             /* unnecessary test */
0x00007fdd859dbb93: jge    0x00007fdd859dbb80    /* go back to the loop top */

它作弊,很难,当你使用s;首先,有一些我不认为理解的拧巴,但看起来像是一个展开的循环的设置:int

0x00007f3dc290b5a1: mov    %r11d,%r9d
0x00007f3dc290b5a4: dec    %r9d
0x00007f3dc290b5a7: mov    %r9d,0x258(%r10)
0x00007f3dc290b5ae: test   %r9d,%r9d
0x00007f3dc290b5b1: jl     0x00007f3dc290b662
0x00007f3dc290b5b7: add    $0xfffffffffffffffe,%r11d
0x00007f3dc290b5bb: mov    %r9d,%ecx
0x00007f3dc290b5be: dec    %ecx              
0x00007f3dc290b5c0: mov    %ecx,0x258(%r10)   
0x00007f3dc290b5c7: cmp    %r11d,%ecx
0x00007f3dc290b5ca: jle    0x00007f3dc290b5d1
0x00007f3dc290b5cc: mov    %ecx,%r9d
0x00007f3dc290b5cf: jmp    0x00007f3dc290b5bb
0x00007f3dc290b5d1: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b5d5: mov    %r9d,%r8d
0x00007f3dc290b5d8: neg    %r8d
0x00007f3dc290b5db: sar    $0x1f,%r8d
0x00007f3dc290b5df: shr    $0x1f,%r8d
0x00007f3dc290b5e3: sub    %r9d,%r8d
0x00007f3dc290b5e6: sar    %r8d
0x00007f3dc290b5e9: neg    %r8d
0x00007f3dc290b5ec: and    $0xfffffffffffffffe,%r8d
0x00007f3dc290b5f0: shl    %r8d
0x00007f3dc290b5f3: mov    %r8d,%r11d
0x00007f3dc290b5f6: neg    %r11d
0x00007f3dc290b5f9: sar    $0x1f,%r11d
0x00007f3dc290b5fd: shr    $0x1e,%r11d
0x00007f3dc290b601: sub    %r8d,%r11d
0x00007f3dc290b604: sar    $0x2,%r11d
0x00007f3dc290b608: neg    %r11d
0x00007f3dc290b60b: and    $0xfffffffffffffffe,%r11d
0x00007f3dc290b60f: shl    $0x2,%r11d
0x00007f3dc290b613: mov    %r11d,%r9d
0x00007f3dc290b616: neg    %r9d
0x00007f3dc290b619: sar    $0x1f,%r9d
0x00007f3dc290b61d: shr    $0x1d,%r9d
0x00007f3dc290b621: sub    %r11d,%r9d
0x00007f3dc290b624: sar    $0x3,%r9d
0x00007f3dc290b628: neg    %r9d
0x00007f3dc290b62b: and    $0xfffffffffffffffe,%r9d
0x00007f3dc290b62f: shl    $0x3,%r9d
0x00007f3dc290b633: mov    %ecx,%r11d
0x00007f3dc290b636: sub    %r9d,%r11d
0x00007f3dc290b639: cmp    %r11d,%ecx
0x00007f3dc290b63c: jle    0x00007f3dc290b64f
0x00007f3dc290b63e: xchg   %ax,%ax /* OK, fine; I know what a nop looks like */

然后是展开的循环本身:

0x00007f3dc290b640: add    $0xfffffffffffffff0,%ecx
0x00007f3dc290b643: mov    %ecx,0x258(%r10)
0x00007f3dc290b64a: cmp    %r11d,%ecx
0x00007f3dc290b64d: jg     0x00007f3dc290b640

然后是展开循环的拆解代码,它本身就是一个测试和一个直线循环:

0x00007f3dc290b64f: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b652: jle    0x00007f3dc290b662
0x00007f3dc290b654: dec    %ecx
0x00007f3dc290b656: mov    %ecx,0x258(%r10)
0x00007f3dc290b65d: cmp    $0xffffffffffffffff,%ecx
0x00007f3dc290b660: jg     0x00007f3dc290b654

因此,对于 ints 来说,它的速度快了 16 倍,因为 JIT 展开了 16 次循环,但根本没有展开循环。intlong

为了完整起见,这是我实际尝试的代码:

public class foo136 {
  private static int i = Integer.MAX_VALUE;
  public static void main(String[] args) {
    System.out.println("Starting the loop");
    for (int foo = 0; foo < 100; foo++)
      doit();
  }

  static void doit() {
    i = Integer.MAX_VALUE;
    long startTime = System.currentTimeMillis();
    while(!decrementAndCheck()){
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Finished the loop in " + (endTime - startTime) + "ms");
  }

  private static boolean decrementAndCheck() {
    return --i < 0;
  }
}

程序集转储是使用选项 生成的。请注意,您需要搞砸JVM安装才能为您完成这项工作;您需要将一些随机共享库放在正确的位置,否则它将失败。-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly


答案 2

JVM 堆栈是根据定义的,其大小是实现细节,但必须至少为 32 位宽。JVM实现者可能使用64位字,但字节码不能依赖这一点,因此必须格外小心地处理带有或值的操作。特别是,JVM整数分支指令完全在类型上定义。longdoubleint

对于您的代码,反汇编是有启发性的。以下是由 Oracle JDK 7 编译的版本字节码:int

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:I
     3: iconst_1      
     4: isub          
     5: dup           
     6: putstatic     #14  // Field i:I
     9: ifge          16
    12: iconst_1      
    13: goto          17
    16: iconst_0      
    17: ireturn       

请注意,JVM 将加载静态值 (0),减去一个 (3-4),复制堆栈上的值 (5),然后将其推回变量 (6)。然后,它执行与零比较分支并返回。i

带有 的版本有点复杂:long

private static boolean decrementAndCheck();
  Code:
     0: getstatic     #14  // Field i:J
     3: lconst_1      
     4: lsub          
     5: dup2          
     6: putstatic     #14  // Field i:J
     9: lconst_0      
    10: lcmp          
    11: ifge          18
    14: iconst_1      
    15: goto          19
    18: iconst_0      
    19: ireturn       

首先,当 JVM 复制堆栈上的新值 (5) 时,它必须复制两个堆栈单词。在你的例子中,这很有可能并不比复制一个更昂贵,因为JVM在方便的情况下可以自由地使用64位单词。但是,您会注意到此处的分支逻辑更长。JVM没有将a与零进行比较的指令,因此它必须将常量推送到堆栈(9)上,进行一般比较(10),然后在计算的值上进行分支。long0Llong

以下是两种合理的方案:

  • JVM 完全遵循字节码路径。在本例中,它在版本中执行更多工作,推送和弹出几个额外的值,这些值位于虚拟托管堆栈上,而不是真正的硬件辅助 CPU 堆栈上。如果是这种情况,预热后您仍会看到显著的性能差异。long
  • JVM 意识到它可以优化此代码。在这种情况下,需要额外的时间来优化一些几乎不必要的推送/比较逻辑。如果是这种情况,您将在热身后看到非常小的性能差异。

我建议你写一个正确的微基准标记,以消除让JIT启动的影响,并在最终条件不为零的情况下尝试这样做,以强制JVM对.intlong


推荐