Hotspot JVM 如何处理 x86 上的整数除法溢出?
How does Hotspot JVM handle integer divison overflow on x86?
在Java中划分两个int
没有什么特别的。除非处理了两种特殊情况之一:
- 除以零。 (JVMS要求虚拟机抛
ArithmeticException
)
- 除法溢出(
Integer.MIN_VALUE / -1
,JVMS要求结果等于Integer.MIN_VALUE
)(此题专门针对本例).
来自 Chapter 6. The Java Virtual Machine Instruction Set. idiv
:
There is one special case that does not satisfy this rule: if the dividend is the negative integer of largest possible magnitude for the int
type, and the divisor is -1
, then overflow occurs, and the result is equal to the dividend. Despite the overflow, no exception is thrown in this case.
在我的计算机 (x86_64
) 上,本机除法会产生 SIGFPE
错误。
当我编译以下C代码时:
#include <limits.h>
#include <stdio.h>
int divide(int a, int b) {
int r = a / b;
printf("%d / %d = %d\n", a, b, a / b);
return r;
}
int main() {
divide(INT_MIN, -1);
return 0;
}
我得到结果(在 x86 上):
tmp $ gcc division.c
tmp $ ./a.out
Floating point exception (core dumped)
在 ARM (aarch64
) 上编译的完全相同的代码产生:
-2147483648 / -1 = -2147483648
因此看来在 x86 上 Hotspot VM 需要做额外的工作来处理这种情况。
- 在这种情况下,虚拟机做了什么才不会在已编译代码中损失太多性能?
- 它是否利用了 POSIX 系统中的信号处理可能性?如果是这样,它在 Windows 上使用什么?
What does the virtual machine do in this case to not lose performance
too much in compiled code?
他们什么都不做。它只是作为 if 语句实现的。
基于目标架构有不同的字节码解释器,但我看了看它们的实现都是相同的。 Here's x86
inline jint BytecodeInterpreter::VMintDiv(jint op1, jint op2) {
/* it's possible we could catch this special case implicitly */
if ((juint)op1 == 0x80000000 && op2 == -1) return op1;
else return op1 / op2;
}
我不确定评论在暗示什么。我在 JDK 邮件列表中找不到关于此方法的任何有趣的提及,这是我通常 go-to 如果我想要对某些历史性决定进行解释的话。
总之,重点在'could'这个词上。不管他们的意思是什么,他们都没有这样做。
你是对的 - HotSpot JVM 不能盲目地使用 idiv
cpu 指令因为特殊情况。
因此 JVM 执行额外的检查,是否 Integer.MIN_VALUE
除以 -1
。此检查同时存在于 interpreter and in the compiled code.
如果我们用 -XX:+PrintAssembly
检查实际编译的代码,我们会看到类似
的内容
0x00007f212cc58410: cmp [=10=]x80000000,%eax ; dividend == Integer.MIN_VALUE?
0x00007f212cc58415: jne 0x00007f212cc5841f
0x00007f212cc58417: xor %edx,%edx
0x00007f212cc58419: cmp [=10=]xffffffff,%r11d ; divisor == -1?
0x00007f212cc5841d: je 0x00007f212cc58423
0x00007f212cc5841f: cltd
0x00007f212cc58420: idiv %r11d ; normal case
0x00007f212cc58423: mov %eax,0x70(%rbx)
但是,您可能会注意到,没有检查除数 == 0。这被认为是一种例外情况,在正常程序中永远不会发生。这称为 隐式异常 。 JVM 记录了可能发生这种异常的地方,并依靠 OS 信号(或 Windows 术语中的异常)来处理这种情况。
if (sig == SIGFPE &&
(info->si_code == FPE_INTDIV || info->si_code == FPE_FLTDIV)) {
stub =
SharedRuntime::
continuation_for_implicit_exception(thread,
pc,
SharedRuntime::
IMPLICIT_DIVIDE_BY_ZERO);
但是,如果碰巧在同一个地方经常发生隐式异常,JVM 会取消优化编译代码,然后使用显式零检查重新编译它(以避免频繁信号处理的性能损失)。
在Java中划分两个int
没有什么特别的。除非处理了两种特殊情况之一:
- 除以零。 (JVMS要求虚拟机抛
ArithmeticException
) - 除法溢出(
Integer.MIN_VALUE / -1
,JVMS要求结果等于Integer.MIN_VALUE
)(此题专门针对本例).
来自 Chapter 6. The Java Virtual Machine Instruction Set. idiv
:
There is one special case that does not satisfy this rule: if the dividend is the negative integer of largest possible magnitude for the
int
type, and the divisor is-1
, then overflow occurs, and the result is equal to the dividend. Despite the overflow, no exception is thrown in this case.
在我的计算机 (x86_64
) 上,本机除法会产生 SIGFPE
错误。
当我编译以下C代码时:
#include <limits.h>
#include <stdio.h>
int divide(int a, int b) {
int r = a / b;
printf("%d / %d = %d\n", a, b, a / b);
return r;
}
int main() {
divide(INT_MIN, -1);
return 0;
}
我得到结果(在 x86 上):
tmp $ gcc division.c
tmp $ ./a.out
Floating point exception (core dumped)
在 ARM (aarch64
) 上编译的完全相同的代码产生:
-2147483648 / -1 = -2147483648
因此看来在 x86 上 Hotspot VM 需要做额外的工作来处理这种情况。
- 在这种情况下,虚拟机做了什么才不会在已编译代码中损失太多性能?
- 它是否利用了 POSIX 系统中的信号处理可能性?如果是这样,它在 Windows 上使用什么?
What does the virtual machine do in this case to not lose performance too much in compiled code?
他们什么都不做。它只是作为 if 语句实现的。
基于目标架构有不同的字节码解释器,但我看了看它们的实现都是相同的。 Here's x86
inline jint BytecodeInterpreter::VMintDiv(jint op1, jint op2) {
/* it's possible we could catch this special case implicitly */
if ((juint)op1 == 0x80000000 && op2 == -1) return op1;
else return op1 / op2;
}
我不确定评论在暗示什么。我在 JDK 邮件列表中找不到关于此方法的任何有趣的提及,这是我通常 go-to 如果我想要对某些历史性决定进行解释的话。
总之,重点在'could'这个词上。不管他们的意思是什么,他们都没有这样做。
你是对的 - HotSpot JVM 不能盲目地使用 idiv
cpu 指令因为特殊情况。
因此 JVM 执行额外的检查,是否 Integer.MIN_VALUE
除以 -1
。此检查同时存在于 interpreter and in the compiled code.
如果我们用 -XX:+PrintAssembly
检查实际编译的代码,我们会看到类似
0x00007f212cc58410: cmp [=10=]x80000000,%eax ; dividend == Integer.MIN_VALUE?
0x00007f212cc58415: jne 0x00007f212cc5841f
0x00007f212cc58417: xor %edx,%edx
0x00007f212cc58419: cmp [=10=]xffffffff,%r11d ; divisor == -1?
0x00007f212cc5841d: je 0x00007f212cc58423
0x00007f212cc5841f: cltd
0x00007f212cc58420: idiv %r11d ; normal case
0x00007f212cc58423: mov %eax,0x70(%rbx)
但是,您可能会注意到,没有检查除数 == 0。这被认为是一种例外情况,在正常程序中永远不会发生。这称为 隐式异常 。 JVM 记录了可能发生这种异常的地方,并依靠 OS 信号(或 Windows 术语中的异常)来处理这种情况。
if (sig == SIGFPE &&
(info->si_code == FPE_INTDIV || info->si_code == FPE_FLTDIV)) {
stub =
SharedRuntime::
continuation_for_implicit_exception(thread,
pc,
SharedRuntime::
IMPLICIT_DIVIDE_BY_ZERO);
但是,如果碰巧在同一个地方经常发生隐式异常,JVM 会取消优化编译代码,然后使用显式零检查重新编译它(以避免频繁信号处理的性能损失)。