В делении двух int в Java нет ничего особенного. Если не обрабатывается один из двух особых случаев:
- Деление на ноль. (JVMS требует, чтобы виртуальная машина выдавала
ArithmeticException)
- Переполнение деления (
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?
Ответ 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, к которому я обычно обращаюсь, если мне нужно объяснение какого-либо исторического решения.
Во всяком случае, акцент на слове "могут". Что бы они под этим ни подразумевали, они этого не делают.