英文:
How does Hotspot JVM handle integer divison overflow on x86?
问题
关于Java中两个整数相除的特殊情况,除非其中一个是特殊情况,否则没有什么特殊之处:
- 除零操作(JVMS要求虚拟机抛出
ArithmeticException
) - 除法溢出(
Integer.MIN_VALUE / -1
,JVMS要求结果等于Integer.MIN_VALUE
)(此问题仅涉及此情况)。
> 有一种特殊情况不符合这个规则:如果被除数是int
类型的最大可能幅度的负整数,并且除数是-1
,那么会发生溢出,结果等于被除数。尽管发生了溢出,在这种情况下不会抛出异常。
在我的计算机(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虚拟机需要额外的工作来处理这种情况。
- 虚拟机在这种情况下采取什么措施,以不会在编译代码中损失太多性能?
- 它是否利用了POSIX系统中的信号处理可能性?如果是这样,在Windows上使用了什么?
英文:
There is nothing special about dividing two int
s in Java. Unless one of the two special cases handled:
- Division by zero. (JVMS requires the virtual machine to throw
ArithmeticException
) - Division overflow (
Integer.MIN_VALUE / -1
, JVMS requires the result to be equal toInteger.MIN_VALUE
) (This question is about this case exclusively).
From 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.
On my computer (x86_64
) native division produces a SIGFPE
error.
When I compile the following C code:
#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;
}
I get the result (on x86):
tmp $ gcc division.c
tmp $ ./a.out
Floating point exception (core dumped)
Exactly the same code compiled on ARM (aarch64
) produces:
-2147483648 / -1 = -2147483648
So it seems that on x86 the Hotspot VM is required to do extra work to handle this case.
- What does the virtual machine do in this case to not lose performance too much in compiled code?
- Does it exploit the signal handling possibilities in POSIX systems? If so what does it use on Windows?
答案1
得分: 6
你说得对 - HotSpot JVM 不能盲目使用 idiv
CPU 指令,因为存在特殊情况。
因此,JVM 执行额外的检查,检查 Integer.MIN_VALUE
是否被 -1
整除。这个检查存在于解释器和编译代码中。
如果我们使用 -XX:+PrintAssembly
检查实际的编译代码,会看到类似以下的内容:
0x00007f212cc58410: cmp $0x80000000,%eax ; dividend == Integer.MIN_VALUE?
0x00007f212cc58415: jne 0x00007f212cc5841f
0x00007f212cc58417: xor %edx,%edx
0x00007f212cc58419: cmp $0xffffffff,%r11d ; divisor == -1?
0x00007f212cc5841d: je 0x00007f212cc58423
0x00007f212cc5841f: cltd
0x00007f212cc58420: idiv %r11d ; normal case
0x00007f212cc58423: mov %eax,0x70(%rbx)
然而,正如你可能注意到的,没有检查除数是否等于 0。这被视为异常情况,在正常程序中不应该发生。这被称为隐式异常。JVM 记录了可能发生此类异常的地方,并依赖于操作系统信号(或Windows术语中的异常)来处理此情况。
请参考os_linux_x86.cpp:
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 会取消编译代码,并在之后重新编译它,加入显式的零检查(以避免频繁信号处理的性能惩罚)。
英文:
You are right - HotSpot JVM cannot blindly use idiv
cpu instruction because of the special case.
Hence JVM performs an extra check, whether Integer.MIN_VALUE
is divided by -1
. This check exists both in the interpreter and in the compiled code.
If we check the actual compiled code with -XX:+PrintAssembly
, we'll see something like
0x00007f212cc58410: cmp $0x80000000,%eax ; dividend == Integer.MIN_VALUE?
0x00007f212cc58415: jne 0x00007f212cc5841f
0x00007f212cc58417: xor %edx,%edx
0x00007f212cc58419: cmp $0xffffffff,%r11d ; divisor == -1?
0x00007f212cc5841d: je 0x00007f212cc58423
0x00007f212cc5841f: cltd
0x00007f212cc58420: idiv %r11d ; normal case
0x00007f212cc58423: mov %eax,0x70(%rbx)
However, as you may notice, there is no check for divisor == 0. This is considered an exceptional case, which should never happen in a normal program. This is called an implicit exception. JVM records the place where such exception may happen, and relies on OS signals (or Exceptions in Windows terminology) to handle this case.
See os_linux_x86.cpp:
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);
However, if it happens that an implicit exception occurs too often at the same place, JVM deoptimizes the compiled code, and recompiles it afterwards with the explicit zero check (to avoid performance penalty of frequent signal handling).
答案2
得分: 0
虚拟机在这种情况下为了不损失太多性能并不采取任何措施。它只是作为一个条件语句来实现的。
基于目标架构有不同的字节码解释器,但我查看了它们的实现,在所有架构中都是相同的。这是x86架构的实现
inline jint BytecodeInterpreter::VMintDiv(jint op1, jint op2) {
/* 可能我们可以隐式地捕获这种特殊情况 */
if ((juint)op1 == 0x80000000 && op2 == -1) return op1;
else return op1 / op2;
}
我不确定这个注释指的是什么。在JDK邮件列表中,我找不到任何有趣的提及这个方法的信息,通常我会在那里寻找某些历史决策的解释。
无论如何,重点在于'could'这个词。无论他们的意思是什么,他们都没有这样做。
英文:
> What does the virtual machine do in this case to not lose performance
> too much in compiled code?
They don't do anything. It's just implemented as an if statement.
There are different bytecode interpreters based on the target architecture, but I looked and the implementation is the same for all of them. 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;
}
I'm not sure what the comment is alluding to. I couldn't find any interesting mentions of this method on the JDK mailing list, which is my usual go-to if I want an explanation for some historic decision.
Anyway, emphasis on the word 'could'. Whatever they meant by it, they don't do it.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论