Hotspot JVM 如何处理 x86 上的整数除法溢出?

How does Hotspot JVM handle integer divison overflow on x86?

在Java中划分两个int没有什么特别的。除非处理了两种特殊情况之一:

  1. 除以零。 (JVMS要求虚拟机抛ArithmeticException
  2. 除法溢出(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 需要做额外的工作来处理这种情况。

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 术语中的异常)来处理这种情况。

参见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 会取消优化编译代码,然后使用显式零检查重新编译它(以避免频繁信号处理的性能损失)。