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

时间:2013-11-07 18:41:39

标签: 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;
    }

}

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

在64位释放模式下: long: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;



}

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

8 个答案:

答案 0 :(得分:79)

当你使用long时,我的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时,它很难作弊;首先是一些我声称不理解的螺旋,但看起来像是一个展开循环的设置:

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

因此,对于整数来说,它的速度提高了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;
  }
}

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

答案 1 :(得分:21)

JVM堆栈是根据定义的,其大小是实现细节但必须至少为32位宽。 JVM实现者可能使用64位字,但字节码不能依赖于此,因此必须小心处理longdouble值的操作。特别是,the JVM integer branch instructions的定义与int类型完全相同。

对于您的代码,反汇编是有益的。这是由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),然后将其推回变量(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与零进行比较的指令,因此必须将常量0L推入堆栈(9),进行一般long比较(10) ,然后分支 计算的值。

以下是两种看似合理的情景:

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

我建议你write a correct microbenchmark消除JIT启动的影响,并尝试使用非零的最终条件,强制JVM对{{1进行相同的比较它与int

有关

答案 2 :(得分:7)

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

底层架构并未规定字大小也应相同。 JVM逐字读/写数据。这就是为什么 long int 花费更长时间的原因。

Here您可以在同一主题上找到更多信息。

答案 3 :(得分:4)

我刚刚使用caliper编写了一个基准。

results与原始代码完全一致:使用int超过long的速度提高了约12倍。肯定看起来循环展开reported by tmyklebu或类似的东西正在发生。

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

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

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;
    }
}

答案 4 :(得分:1)

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

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%,但两者之间的比例大致保持不变。

答案 5 :(得分:1)

记录:

如果我使用

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

(将“l--”更改为“l = l - 1l”)长期性能提高约50%

答案 6 :(得分:1)

这可能是由于JVM在使用long时(无计数循环)检查安全点,而不是为int(有计数循环)检查安全点。

一些参考: https://stackoverflow.com/a/62557768/14624235

https://stackoverflow.com/a/58726530/14624235

http://psy-lob-saw.blogspot.com/2016/02/wait-for-it-counteduncounted-loops.html

答案 7 :(得分:0)

我没有要测试的64位机器,但是相当大的差异表明工作中存在的字节码略长。

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

这只是一个 guess ,但我强烈怀疑它是内存未对齐惩罚的影响。要确认/否认怀疑,请尝试添加public static int dummy = 0; 之前声明i。这将使我在内存布局中减少4个字节,并可以使其正确对齐以获得更好的性能。确认不会导致问题。

编辑:这背后的原因是VM可能不会在其休闲时添加填充以重新排序字段以实现最佳对齐,因为这可能会干扰JNI (不是这种情况) )。