【问题标题】:How does Hotspot JVM handle integer divison overflow on x86?Hotspot JVM 如何处理 x86 上的整数除法溢出?
【发布时间】:2020-08-10 04:08:10
【问题描述】:

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

  1. 除以零。 (JVMS要求虚拟机抛出ArithmeticException
  2. 除法溢出(Integer.MIN_VALUE / -1,JVMS 要求结果等于Integer.MIN_VALUE)(这个问题专门针对这种情况)

来自Chapter 6. The Java Virtual Machine Instruction Set. idiv

有一种特殊情况不满足这个规则:如果被除数是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 VM 似乎需要做额外的工作来处理这种情况。

  • 在这种情况下,虚拟机如何在编译代码中不损失太多性能?
  • 它是否利用了 POSIX 系统中的信号处理可能性?如果是这样,它在 Windows 上使用什么?

【问题讨论】:

    标签: java jvm jvm-hotspot


    【解决方案1】:

    你说得对——HotSpot JVM 不能因为特殊情况而盲目使用idiv cpu 指令。

    因此 JVM 执行额外的检查,Integer.MIN_VALUE 是否除以 -1。此检查存在于interpretercompiled code 中。

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

    【讨论】:

      【解决方案2】:

      在这种情况下,虚拟机做了什么才能不损失性能 编译代码太多?

      他们什么都不做。它只是作为 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 邮件列表中找不到任何有趣的提及该方法的内容,如果我想对某些历史性决定进行解释,我通常会使用此方法。

      无论如何,强调“可以”这个词。不管他们的意思是什么,他们都不会这样做。

      【讨论】:

      • 您引用的代码从不在常规 HotSpot 构建中执行。此代码是“C++ 解释器”的一部分 - 用于各种测试和实验的玩具“零汇编”解释器。普通的 JDK 二进制文件甚至不包含此代码。实际执行的代码是Template Interpreter
      猜你喜欢
      • 1970-01-01
      • 2011-08-12
      • 1970-01-01
      • 1970-01-01
      • 2017-03-25
      • 2013-06-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多