Search code examples
javajvmjvm-hotspot

How does Hotspot JVM handle integer divison overflow on x86?


There is nothing special about dividing two ints in Java. Unless one of the two special cases handled:

  1. Division by zero. (JVMS requires the virtual machine to throw ArithmeticException)
  2. Division overflow (Integer.MIN_VALUE / -1, JVMS requires the result to be equal to Integer.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?

Solution

  • 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).