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


90

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

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

我唯一的猜测是,与64位整数相比,CPU需要更长的时间来添加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内部: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;



}

编辑:刚刚在Java 8 RTM中再次尝试了此操作,没有重大更改。


8
最可能的怀疑是您的设置,而不是CPU或JVM的各个部分。您可以可靠地重现此测量结果吗?不重复循环,不预热JIT,使用currentTimeMillis(),可以完全优化地运行代码,等等,结果不可靠。

1
我之前进行了基准测试,我不得不使用a long作为循环计数器,因为当我使用时,JIT编译器优化了循环输出int。人们需要查看所生成机器代码的反汇编。
山姆

7
这不是正确的微基准测试,我不希望它的结果以任何方式反映现实。
Louis Wasserman

7
指责OP未能写出适当的Java微基准测试的所有评论都是懒惰的。如果您只是看一下JVM对代码做了什么,这种事情很容易弄清楚。
tmyklebu

2
@maaartinus:接受的实践是接受的实践,因为它可以解决一系列已知的陷阱。对于正确的Java基准测试,您要确保您正在评估的是正确优化的代码,而不是堆栈上的替代品,并且您希望确保最后的度量是干净的。OP发现了一个完全不同的问题,他提供的基准充分证明了这一点。而且,如前所述,将这些代码转换为适当的Java Benchmark并不会真正消除问题。而且阅读汇编代码并不难。
tmyklebu

Answers:


80

当您使用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 */

当您使用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 */

然后展开循环本身:

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

因此,对于int而言,速度要快16倍,因为JIT展开了int16次循环,但没有展开循环。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;
  }
}

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


8
好的,因此网络不是long版本较慢,而是int版本较快。这就说得通了。在制作JIT优化long表达式上可能没有投入太多精力。
2013年

1
...原谅我的无知,但是什么是“无趣的”呢?我什至似乎都无法正确地使用Google这个词,这使我第一次不得不问某人互联网上的单词是什么意思。
BrianH

1
@BrianDHall gcc用途-f为“标志”的命令行开关,并且unroll-loops优化说接通-funroll-loops。我只是使用“展开”来描述优化。
克莱里斯-cautiouslyoptimistic-

4
@BRPocock:Java编译器不能,但是JIT当然可以。
tmyklebu

1
只是为了清楚起见,它没有使它“有趣”。它展开了它,并将展开的循环转换为i-=16,这当然快了16倍。
Aleksandr Dubinsky

22

JVM堆栈是用word定义的,其大小是实现细节,但必须至少为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       

请注意,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意识到可以优化此代码。在这种情况下,要花费一些时间来优化一些实际上不必要的推入/比较逻辑。在这种情况下,预热后您几乎看不到性能差异。

我建议你写一个正确的微基准,以消除其在JIT踢,也与不为零的最终条件尝试这个,迫使JVM上做了相同的比较的效果int,它与做long


1
@Katona不一定。尤其是,客户端和服务器HotSpot JVM是完全不同的实现,Ilya并未指出选择服务器(客户端通常是默认的32位)。
克莱里斯-cautiouslyoptimistic-

1
@tmyklebu问题在于基准测试一次测量了几种不同的东西。使用非零终止条件会减少变量的数量。
克莱里斯-cautiouslyoptimistic-

1
@tmyklebu关键是OP打算比较int vs longs的增量,减量和比较速度。相反(假设此答案正确),他们仅衡量比较,并且仅针对0(这是特例)。如果没有其他原因,它会使原始基准产生误导作用-看起来它衡量了三种一般情况,而实际上却衡量了一种具体情况。
yshavit 2013年

1
@tmyklebu别误会我,我赞成这个问题,这个答案和你的答案。但我不同意您的说法,即@chrylis正在调整基准,以停止衡量其试图衡量的差异。如果我错了,OP可以纠正我,但是看起来他们并没有尝试仅/主要地衡量== 0,这似乎在基准测试结果中占了很大比例。在我看来,OP更有可能尝试衡量更广泛的操作范围,而该答案指出,基准测试高度偏向于其中一项操作。
yshavit 2013年

2
@tmyklebu一点也不。我全都是为了了解根本原因。但是,在确定了一个主要的根本原因是基准存在偏斜之后,更改基准以消除偏斜以及挖掘和更多地了解该偏斜并不是无效的(例如,它可以使效率更高字节码,这样可以更轻松地展开循环等)。这就是为什么我都赞成这个答案(确定了偏斜)和你的答案(更详细地探究了偏斜)的原因。
yshavit 2013年

8

Java虚拟机中数据的基本单位是word。选择正确的字长取决于JVM的实现。JVM实现应选择最小字长为32位。它可以选择较高的字长以提高效率。对于64位JVM仅选择64位字,也没有任何限制。

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

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


4

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

结果与原来的代码相当一致:一〜12倍的加速使用intlong。显然,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;
    }
}

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%,但两者之间的比率保持大致相同。


@TedHopp-我尝试更改我的循环限制,它基本上保持不变。
2013年

@ Techrocket9:我int用这段代码得到了相似的数字(快了20倍)。
tmyklebu

1

记录:

如果我用

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

(将“ l--”更改为“ l = 1-1l”),长期效果提高了约50%


0

我没有要测试的64位计算机,但是相差很大,这表明工作的字节码要多得多。

我在32位1.7.0_45上看到了很长的时间(整数/整数(4400 vs 4800ms))。

这只是一个猜测,但我强烈怀疑这是内存未对齐惩罚的结果。为了确认/否认这种怀疑,请尝试添加一个公共static int dummy = 0; 我宣布之前。这将使i在内存布局中下降4个字节,并可能使其正确对齐以提高性能。 确认不是引起问题的原因。

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


虚拟机肯定不允许重新排序字段,添加填充。
2013年

JNI必须通过这些烦人的,慢速的访问器方法访问对象,这些方法无论如何都需要一些不透明的句柄,因为在本机代码运行时GC可能发生。重新排列字段并添加填充是完全免费的。
tmyklebu 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.