[英]Why is long slower than int in x64 Java?
我在 Surface Pro 2 平板电脑上运行带有 Java 7 更新 45 x64(未安装 32 位 Java)的 Windows 8.1 x64。
当 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++ 代码的结果。 长:72265ms 整数:74656ms 这些结果处于调试 32 位模式。
在 64 位发布模式下: 长:875ms 长长:906ms 整数: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
时,它很难作弊; 首先有一些我没有声称理解但看起来像展开循环的设置:
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 安装才能使这项工作也适合您; 您需要将一些随机共享库放在正确的位置,否则它将失败。
JVM 堆栈是根据words定义的,其大小是实现细节,但必须至少为 32 位宽。 JVM 实现者可能使用 64 位字,但字节码不能依赖于此,因此必须特别小心处理具有long
或double
值的操作。 特别是, JVM 整数分支指令完全定义在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
与 0 进行比较的指令,因此它必须将常量0L
压入堆栈 (9),进行常规long
比较 (10),然后对该计算的值进行分支。
以下是两种可能的情况:
long
版本中做了更多的工作,推送和弹出几个额外的值,这些在虚拟托管堆栈上,而不是真正的硬件辅助 CPU 堆栈。 如果是这种情况,您在预热后仍会看到显着的性能差异。 我建议你写一个正确的微基准,以消除其在JIT踢,也与不为零的最终条件尝试这个,迫使JVM上做了相同的比较效果int
它与不long
.
Java 虚拟机中数据的基本单位是字。 选择正确的字长取决于 JVM 的实现。 JVM 实现应选择 32 位的最小字长。 它可以选择更高的字长来提高效率。 也没有任何限制 64 位 JVM 只能选择 64 位字。
底层架构并不规定字长也应该相同。 JVM 逐字读取/写入数据。 than an .这就是为什么它可能需要更长的时间超过一个的原因。
在这里,您可以找到有关同一主题的更多信息。
我刚刚使用caliper编写了一个基准测试。
结果与原始代码非常一致:在long
使用int
速度提高了约 12 倍。 看来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;
}
}
为了记录,这个版本做了一个粗略的“热身”:
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%,但两者之间的比率大致保持不变。
对于记录:
如果我使用
boolean decrementAndCheckLong() {
lo = lo - 1l;
return lo < -1l;
}
(将“l--”更改为“l = l - 1l”)长时间性能提高了约 50%
这可能是由于 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
我没有要测试的 64 位机器,但相当大的差异表明,工作中的字节码不仅仅是稍长的字节码。
我在 32 位 1.7.0_45 上看到 long/int(4400 对 4800 毫秒)的时间非常接近。
这只是一个猜测,但我强烈怀疑这是内存未对齐惩罚的影响。 要确认/否认怀疑,请尝试添加一个 public static int dummy = 0; 在i 声明之前。 这将在内存布局中将 i 向下推 4 个字节,并可能使其正确对齐以获得更好的性能。 确认不是导致问题的原因。
编辑: 这背后的原因是 VM 可能不会在空闲时重新排序字段添加填充以获得最佳对齐,因为这可能会干扰 JNI (并非如此)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.