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

我在Surface Pro 2平板电脑上运行Windows 7 x64,Java 7更新为45 x64(没有安装32位Java)。

下面的代码需要1688毫秒,当我是一个长的types和109毫秒时,我是一个整数。 为什么在64位JVM的64位平台上,long(64位types)比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 ++代码的结果。 长:72265ms int:74656ms 这些结果是在debugging32位模式。

在64位版本模式下: 长:875ms 长长: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; } 

编辑:只是在Java 8 RTM再次尝试,没有显着的变化。

当你使用long s时,我的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 */ 

当你使用int s的时候,它很难作弊; 首先有一些我不明白但是看起来像展开循环的设置:

 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 

那么展开循环的拆卸代码本身就是一个testing和一个直线循环:

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

因此,它的整数增加了16倍,因为JIT展开了16次int循环,但是根本没有展开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; } } 

程序集转储是使用选项-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly 。 请注意,您需要解决您的JVM安装问题,以便为您完成此项工作。 你需要把一些随机的共享库放在正确的地方,否则会失败。

JVM堆栈是用来定义的,其大小是一个实现细节,但必须至less为32位宽。 JVM实现者可以使用64位字,但是字节码不能依赖于这个字,所以具有long或者double值的操作必须格外小心。 尤其是, JVM整数分支指令正好在inttypes上定义。

在你的代码的情况下,反汇编是有启发性的。 以下是由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将加载您的静态i (0)的值,减去一个(3-4),复制堆栈(5)的值,并将其推回到variables(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 

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

以下是两种可能的情况:

  • JVM完全遵循字节码path。 在这种情况下,它在long版本中执行更多的工作,推送和popup一些额外的值,这些值位于虚拟pipe理堆栈上 ,而不是真正的硬件辅助CPU堆栈。 如果是这种情况,热身后你仍然会看到显着的性能差异。
  • JVM意识到它可以优化这个代码。 在这种情况下,需要花费额外的时间去优化一些实际上不必要的推/比较逻辑。 如果是这种情况,热身后你会看到很less的性能差异。

我build议你写一个正确的微基准来消除JIT踢入的效果,并且试图用最终条件不是零来强制JVM对int进行相同的比较, 。

Java虚拟机中基本的数据单元是单词。 在JVM的实现中select正确的字大小。 JVM实现应该select32位的最小字长。 它可以select更高的字大小来提高效率。 64位JVM只能select64位字,这也没有任何限制。

底层体系结构并不规定字大小也应该是相同的。 JVM逐字读取/写入数据。 这就是为什么它可能需要更长的时间整数

在这里你可以find更多关于同一主题。

我刚刚用卡尺写了一个基准。

结果与原始代码非常一致:使用int long速度提高了12倍。 当然似乎tmyklebu报道的循环展开或类似的事情正在进行。

 timeIntDecrements 195,266,845.000 timeLongDecrements 2,321,447,978.000 

这是我的代码; 请注意,它使用了新build的caliper快照,因为我无法弄清楚如何针对现有的testing版进行编码。

 package test; import com.google.caliper.Benchmark; import com.google.caliper.Param; public final class App { @Param({""+1}) int number; private static class IntTest { public static int v; public static void reset() { v = Integer.MAX_VALUE; } public static boolean decrementAndCheck() { return --v < 0; } } private static class LongTest { public static long v; public static void reset() { v = Integer.MAX_VALUE; } public static boolean decrementAndCheck() { return --v < 0; } } @Benchmark int timeLongDecrements(int reps) { int k=0; for (int i=0; i<reps; i++) { LongTest.reset(); while (!LongTest.decrementAndCheck()) { k++; } } return (int)LongTest.v | k; } @Benchmark int timeIntDecrements(int reps) { int k=0; for (int i=0; i<reps; i++) { IntTest.reset(); while (!IntTest.decrementAndCheck()) { k++; } } return IntTest.v | k; } } 

为了logging,这个版本做了一个粗略的“热身”:

 public class LongSpeed { private static long i = Integer.MAX_VALUE; private static int j = Integer.MAX_VALUE; public static void main(String[] args) { for (int x = 0; x < 10; x++) { runLong(); runWord(); } } private static void runLong() { System.out.println("Starting the long loop"); i = Integer.MAX_VALUE; long startTime = System.currentTimeMillis(); while(!decrementAndCheckI()){ } long endTime = System.currentTimeMillis(); System.out.println("Finished the long loop in " + (endTime - startTime) + "ms"); } private static void runWord() { System.out.println("Starting the word loop"); j = Integer.MAX_VALUE; long startTime = System.currentTimeMillis(); while(!decrementAndCheckJ()){ } long endTime = System.currentTimeMillis(); System.out.println("Finished the word loop in " + (endTime - startTime) + "ms"); } private static boolean decrementAndCheckI() { return --i < 0; } private static boolean decrementAndCheckJ() { return --j < 0; } } 

总体时间提高了30%左右,但两者的比例大致相同。

对于logging:

如果我使用

 boolean decrementAndCheckLong() { lo = lo - 1l; return lo < -1l; } 

(改变“l–”到“l = l-1l”)长时间的性能提高了〜50%

我没有一个64位的机器来testing,但是相当大的差异表明,在工作中有更多的字节码。

我在32位1.7.0_45上看到long / int(4400 vs 4800ms)非常接近的时间。

这只是一个猜测 ,但我强烈怀疑这是一个记忆错位罚款的影响。 要确认/否认怀疑,请尝试添加一个公共静态诠释= 0; 宣布之前 。 这将推我在内存布局的4个字节,并可能使其正确alignment,以获得更好的性能。 证实不是造成这个问题。

编辑: 这背后的原因是虚拟机可能不会重新sorting字段在休闲添加填充最佳alignment,因为这可能会干扰JNI (不是这样)。