为什么在x64 Java中长度比int慢?

Tec*_*et9 90 java performance 32bit-64bit long-integer

我在Surface Pro 2平板电脑上运行带有Java 7更新45 x64(没有安装32位Java)的Windows 8.1 x64.

当i的类型为long时,下面的代码需要1688ms,当i是int时,代码需要109ms.为什么在具有64位JVM的64位平台上,long(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;
    }

}
Run Code Online (Sandbox Code Playgroud)

编辑:以下是VS 2013(下面),同一系统编译的等效C++代码的结果. 长:72265ms int:74656ms 这些结果是在调试32位模式下.

在64位发布模式下: 长:875ms long long:906ms int:1047ms

这表明我观察到的结果是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;



}
Run Code Online (Sandbox Code Playgroud)

编辑:刚刚在Java 8 RTM中尝试过这个,没有重大改变.

tmy*_*ebu 79

当你使用longs 时,我的JVM对内循环做了这个非常简单的事情:

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

当你使用ints 时,它很难欺骗; 首先是一些我声称不理解的螺旋,但看起来像是一个展开的循环的设置:

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

那么展开的循环本身:

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

那么展开循环的拆解代码,本身就是一个测试和一个直接的循环:

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

因此,对于整数来说,它的速度提高了16倍,因为JIT将int循环展开了16次,但根本没有展开long循环.

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

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

使用选项生成程序集转储-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly.请注意,您需要弄乱JVM安装,以便让您也能使用它; 你需要将一些随机共享库放在正确的位置,否则它将失败.

  • 好的,所以net-net不是`long`版本慢,而是`int`版本更快.那讲得通.可能没有投入太多精力来使JIT优化`long`表达式. (8认同)
  • @BRPocock:Java编译器不能,但JIT肯定可以. (4认同)

chr*_*ke- 22

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

对于您的代码,反汇编是有益的.这是intOracle JDK 7编译的版本的字节码:

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

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

带有的版本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       
Run Code Online (Sandbox Code Playgroud)

首先,当JVM在堆栈(5)上复制新值时,它必须复制两个堆栈字.在你的情况下,很可能这并不比复制一个贵,因为如果方便,JVM可以自由使用64位字.但是,您会注意到分支逻辑在这里更长.JVM没有将a long与零进行比较的指令,因此必须将常量0L推入堆栈(9),进行一般long比较(10),然后对该计算的值进行分支.

这是两个看似合理的场景:

  • JVM完全遵循字节码路径.在这种情况下,它在long版本中做了更多的工作,推送和弹出几个额外的值,这些值在虚拟托管堆栈上,而不是真正的硬件辅助CPU堆栈.如果是这种情况,您在热身后仍会看到显着的性能差异.
  • JVM意识到它可以优化此代码.在这种情况下,它需要额外的时间来优化一些实际上不必要的推/比较逻辑.如果是这种情况,那么在热身后你会发现很少的性能差异.

我建议你写一个正确的microbenchmark来消除JIT启动的影响,并且尝试使用非零的最终条件,强制JVM int对它进行相同的比较long.

  • @tmyklebu完全没有.我全都是为了解根本原因.但是,确定了一个主要根本原因是基准测试是偏斜的,改变基准以消除偏差并不是无效的,以及深入挖掘和理解更多关于这种偏差(例如,它可以提高效率)字节码,它可以更容易展开循环等).这就是为什么我对这个答案(确定了歪斜)和你的答案(更详细地深入研究)倾斜. (2认同)

Vai*_*Raj 8

Java虚拟机中的基本数据单元是单词.选择正确的字大小留在JVM的实现上.JVM实现应选择最小字长为32位.它可以选择更高的字大小来提高效率.64位JVM不应该只选择64位字.

底层架构并未规定字大小也应相同.JVM逐字读/写数据.这就是为什么它可能需要更长的时间而不是int.

在这里您可以找到有关同一主题的更多信息.