为啥在 x64 Java 中 long 比 int 慢?

Posted

技术标签:

【中文标题】为啥在 x64 Java 中 long 比 int 慢?【英文标题】:Why is long slower than int in x64 Java?为什么在 x64 Java 中 long 比 int 慢? 【发布时间】:2013-11-19 13:50:53 【问题描述】:

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

当 i 的类型是 long 时,下面的代码需要 1688 毫秒,而当 i 是 int 时,需要 109 毫秒。为什么在带有 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 中再次尝试过,没有重大变化。

【问题讨论】:

最有可能的嫌疑人是您的设置,而不是 CPU 或 JVM 的各个部分。你能可靠地重现这个测量吗?不重复循环、不预热 JIT、使用 currentTimeMillis()、运行可以轻松优化的代码等等,这些都是不可靠的结果。 前段时间我在进行基准测试,我不得不使用long 作为循环计数器,因为当我使用int 时,JIT 编译器优化了循环输出。需要查看生成的机器代码的反汇编。 这不是一个正确的微基准,我不希望它的结果以任何方式反映现实。 所有批评 OP 未能编写适当的 Java 微基准测试的 cmets 都是无法形容的懒惰。如果您只看一下 JVM 对代码的作用,就很容易弄清楚这一点。 @maaartinus:被接受的做法就是被接受的做法,因为它可以解决一系列已知陷阱。就 Proper Java Benchmarks 而言,您希望确保您测量的是经过适当优化的代码,而不是堆栈上的替换,并且您希望确保您的测量结果是干净的。 OP 发现了一个完全不同的问题,他提供的基准充分证明了这一点。而且,如前所述,将这段代码转换为适当的 Java 基准实际上并不能消除怪异之处。而且阅读汇编代码并不难。 【参考方案1】:

当您使用 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 将 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 安装才能让这项工作也为您服务;您需要将一些随机共享库放在正确的位置,否则它会失败。

【讨论】:

好的,所以net-net并不是long版本更慢,而是int版本更快。这就说得通了。在使 JIT 优化 long 表达式方面投入的精力可能并不多。 ...原谅我的无知,但什么是“funrolled”?我什至无法正确地用谷歌搜索这个词,这使得我第一次不得不在互联网上询问某人一个词的含义。 @BrianDHall gcc 使用-f 作为“标志”的命令行开关,unroll-loops 优化通过说-funroll-loops 开启。我只是用“unroll”来描述优化。 @BRPocock:Java 编译器不能,但 JIT 肯定可以。 为了清楚起见,它并没有“娱乐”它。它展开它并将展开的循环转换为i-=16,这当然快了 16 倍。【参考方案2】:

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 对 int 进行相同的比较与long 一起使用。

【讨论】:

@Katona 不一定。尤其是,Client 和 Server HotSpot JVM 是完全不同的实现,Ilya 并没有表示选择 Server(Client 通常是 32 位默认值)。 @tmyklebu 问题是基准测试同时测量几个不同的东西。使用非零终止条件可以减少变量的数量。 @tmyklebu 关键是 OP 打算比较整数与长整数的增量、减量和比较速度。相反(假设这个答案是正确的)他们只测量比较,并且只针对 0,这是一种特殊情况。如果不出意外,它会使原始基准具有误导性——看起来它测量了三种一般情况,而实际上它测量的是一种特定情况。 @tmyklebu 不要误会我的意思,我赞成这个问题,这个答案和你的答案。但我不同意你的说法,即@chrylis 正在调整基准以停止测量它试图测量的差异。如果我错了,OP 可以纠正我,但看起来他们并不是在尝试仅/主要测量 == 0,这似乎是基准测试结果中不成比例的重要部分。在我看来,OP 更有可能试图衡量更广泛的操作范围,并且这个答案指出基准高度偏向于其中一个操作。 @tmyklebu 一点也不。我完全赞成了解根本原因。但是,在确定了一个主要的根本原因是基准偏差之后,更改基准以消除偏差并以及深入了解并了解有关该偏差的更多信息并不是无效的(例如,它可以启用更有效的字节码,它可以更容易展开循环等)。这就是为什么我赞成这个答案(它确定了偏差)和你的答案(它更详细地挖掘了偏差)。【参考方案3】:

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

底层架构不规定字长也应该相同。 JVM 逐字读取/写入数据。这就是为什么 long 可能比 int 花费更长的时间的原因。

Here你可以找到更多关于同一主题的信息。

【讨论】:

【参考方案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;
    

【讨论】:

【参考方案5】:

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

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 - 我尝试更改我的循环限制,但它基本上保持不变。 @Techrocket9:我用这段代码得到了相似的数字(int 快了 20 倍)。【参考方案6】:

记录:

如果我使用

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

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

【讨论】:

【参考方案7】:

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

一些参考资料: https://***.com/a/62557768/14624235

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

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

【讨论】:

【参考方案8】:

我没有要测试的 64 位机器,但相当大的差异表明工作中的字节码不止稍长一些。

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

这只是一个猜测,但我强烈怀疑这是内存错位惩罚的结果。要确认/否认怀疑,请尝试添加 public static int dummy = 0; i 的声明之前。这会将内存布局中的 i 向下推 4 个字节,并可能使其正确对齐以获得更好的性能。 已确认不会导致问题。

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

【讨论】:

虚拟机确实允许重新排序字段并添加填充。 JNI 必须通过这些烦人的、缓慢的访问器方法访问对象,这些访问器方法无论如何都需要一些不透明的句柄,因为 GC 可能在本机代码运行时发生。重新排序字段和添加填充是免费的。

以上是关于为啥在 x64 Java 中 long 比 int 慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 java.util.zip.CRC32.getValue() 返回一个 long 而不是 int?

为啥Java中的BitSet使用long数组做内部存储,而不使用int数组...

为啥字节和短除法会在 Java 中产生 int?

java long型计算问题

为啥 int 存在于 C 中,为啥不只是 short 和 long

为啥 double 可以存储比 unsigned long long 更大的数字?