Как Hotspot JVM обрабатывает переполнение целочисленного деления на x86?

В делении двух int в Java нет ничего особенного. Если не обрабатывается один из двух особых случаев:

  1. Деление на ноль. (JVMS требует, чтобы виртуальная машина выдавала ArithmeticException)
  2. Переполнение деления (Integer.MIN_VALUE / -1, JVMS требует, чтобы результат был равен Integer.MIN_VALUE) (Этот вопрос относится исключительно к этому случаю).

Из главы 6. ​​Java Набор инструкций виртуальной машины. 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 должна выполнять дополнительную работу, чтобы справиться с этим случаем.

  • Что в этом случае делает виртуальная машина, чтобы не сильно терять производительность в скомпилированном коде?
  • Использует ли он возможности обработки сигналов в системах POSIX? Если да, то что он использует в Windows?

person caco3    schedule 09.08.2020    source источник


Ответы (2)


Вы правы - JVM HotSpot не может слепо использовать инструкцию 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 деоптимизирует скомпилированный код, а затем перекомпилирует его с явной проверкой нуля (чтобы избежать потери производительности из-за частой обработки сигналов).

person apangin    schedule 09.08.2020

Что в этом случае делает виртуальная машина, чтобы не сильно терять производительность в скомпилированном коде?

Они ничего не делают. Он просто реализован как оператор if.

Существуют разные интерпретаторы байт-кода, основанные на целевой архитектуре, но я посмотрел, и реализация у всех одинакова. Вот 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, к которому я обычно обращаюсь, если мне нужно объяснение какого-либо исторического решения.

Во всяком случае, акцент на слове «могут». Что бы они под этим ни подразумевали, они этого не делают.

person Michael    schedule 09.08.2020
comment
Код, на который вы ссылаетесь, никогда не выполняется в обычных сборках HotSpot. Этот код является частью интерпретатора C++ - игрушечного интерпретатора нулевой сборки, используемого в различных тестах и ​​экспериментах. Обычные двоичные файлы JDK даже не включают этот код. Код, который фактически выполняется, представляет собой интерпретатор шаблонов. - person apangin; 09.08.2020