Bezdepbonusu7Программирование: статьи и ответы

Вопрос

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

Автор вопроса: caco3Ответов: 2

В делении двух 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?

javajvmjvm-hotspot

Ответы

Ответ 1 · рейтинг 6 · apangin

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

Ответ 2 · Michael

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

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

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

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы