Как здесь выполняется добавление double и действительный способ определить, что результат не может быть представлен - PullRequest
0 голосов
/ 03 августа 2020

Я пытаюсь понять double немного лучше. В следующем фрагменте кода min и max - это double:

double min = 3.472727272727276;
double max = 3.4727272727272767;
System.out.println(max - min);  
System.out.println((max - min)/2);
double mid = min + ((max - min)/2);
if(min == mid) {
    System.out.println("equal");
}
System.out.println(mid);

Первые 2 оператора печати print:

4.440892098500626E-16
2.220446049250313E-16

Что в основном: 0.0000000000000004440892098500626 и 0.0000000000000002220446049250313

Тогда условная проверка: true, т.е. печатает equal, а последняя печать: 3.472727272727276

Итак, насколько я понимаю, (max - min)/2 дал значение, которое может быть представлено двойным числом. Что мне не ясно, так это то, что происходит во время сложения.

  1. Создает ли добавление число, которое не может быть представлено двойным числом, и оставляет исходный min как есть, отбрасывая цифры или число фактически считается равным 0 до того, как сложение действительно происходит, или как именно это делается?
  2. Является ли min == mid действительной проверкой для обнаружения таких проблем с двойными числами? Т.е. с целым числом мы можем обнаружить переполнение / недостаточное заполнение, проверив, меньше ли результат того, с которого мы начали. Является ли проверка равенства после выполнения добавления разумной / разумной проверкой для обнаружения эквивалентной проблемы с double, то есть того, что добавленное число не было действительно улучшено из-за ошибки округления (или каков именно термин для этого)?

Ответы [ 3 ]

2 голосов
/ 03 августа 2020

В этом примере легко увидеть, что происходит, просмотрев числа в шестнадцатеричном формате с плавающей запятой. Результатом преобразования исходного текста 3.472727272727276 в double будет 3,47272727272727621539161191321909427642822265625, что в шестнадцатеричном формате:

1.BC8253C8253D0<sub>16</sub>•2<sup>1</sup>

Обратите внимание, что в мантиссе ровно 53 бита - один перед "." и 52 в 13 шестнадцатеричных цифрах после него. Формат double имеет один бит для знака, 11 для экспоненты и 53 для мантиссы. (52 хранятся явно; один кодируется через показатель степени.)

Преобразование исходного текста 3.4727272727272767 в double дает 3,472727272727276659480821763281710445880889892578125, что составляет:

1.BC8253C8253D1<sub>16</sub>•2<sup>1</sup>

Теперь мы можем легко посмотрим, что получится с арифметией c на них. Их разница:

0.0000000000001<sub>16</sub>•2<sup>1</sup>

Когда мы нормализуем это, это 1. 16 • 2 1-52 = 1. 16 • 2 -51 ≈ 4,44 • 10 -16 , а формат double может легко представить половину этого, просто изменяя показатель степени. Тогда у нас есть 1. 16 • 2 −52 ≈ 2,22 • 10 −16 .

Однако, когда мы пытаемся сложить половину отличие от первого числа, результат с арифметикой действительных чисел c будет:

1.BC8253C8253D08<sub>16</sub>•2<sup>1</sup>

Обратите внимание, что это 54 бита - один перед «.», затем 52 в 13 шестнадцатеричных цифрах и последний один в старшем бите этого 14 th di git, 8. Формат double не имеет 54 бит в своем значении, поэтому сложение в формате double не может дать такого результата. Вместо этого сумма округляется до ближайшего представимого значения или, в случае ie, до ближайшего представимого значения с четным младшим битом. Таким образом, результат будет 1.BC8253C8253D08 16 • 2 1 , что совпадает с min.

1 голос
/ 03 августа 2020
  1. Создает ли сложение число, которое не может быть представлено двойным числом

Алгоритм сложения двух чисел с плавающей запятой как первый шаг приводит два числа к одному и тому же показателю. Фактически это делается путем сдвига битов меньшего числа вправо, а недостающие биты теряются (становятся нулевыми).

Если расчет выполняется с 64-битной точностью,

3.472727272727276 + 2.220446049250313E-16     or in hex:
0x1.bc8253c8253dp1 + 0x1.0p-52

фактически становится вычислением

3.472727272727276 + 0.0     or in hex:
0x1.bc8253c8253dp1 + 0x0.0p1

, и это происходит аппаратно, поэтому промежуточное значение 0,0 нигде не сохраняется и не отображается как отдельный шаг.

Но: это возможно расчет выполняется с более высокой точностью , чем 64 бит. Например, если доступны инструкции ЦП с плавающей запятой 80-битной точности, JVM может их использовать. В этом случае промежуточные результаты будут другими, но конечный результат все равно останется таким же, потому что результат должен быть сохранен как 64-битное двойное.

Является ли min == mid действительной проверкой для обнаружения таких проблем с двойниками?

Зависит от того, что вам нужно сделать. Оператор == проверяет, равны ли два числа точно , к лучшему или к худшему. Во многих местах люди не хотят точного равенства, потому что его трудно или невозможно достичь: например, Math.sin(Math.PI) не будет точно равным 0, но вы можете предпочесть притвориться, что оно «достаточно близко» к 0.

0 голосов
/ 03 августа 2020

Следующий код может продемонстрировать проблему:

double num = 1;
while (!Double.isInfinite(num)) {
    num *= 2;
    System.out.println(num);
}
System.out.println("-----------------------");
System.out.println("-- now the opposite----");
System.out.println("-----------------------");
num = 1;
while (num > 0) {
    num /= 2;
    System.out.println(num);
}

Пространство в памяти ограничено количеством битов. Таким образом, неизбежно, что в какой-то момент очень маленькое число будет точно равно нулю.

В ваших расчетах операторы действуют на двойные значения, создавая временные двойники в ЦП, которые также подпадают под предел точности и, следовательно, в вашем case становится равным нулю

И, конечно же, оператор == нужно использовать с усердием для удвоений, но здесь проблема не в этом.

Чтобы ответить на второй вопрос, вам нужно использовать BigDecimal вместо double на всякий случай.

Проблема с проверкой заключается в том, что значения, которые может принимать любой тип double, не распределяются равномерно. Между 0 и 1 существует такое же количество значений, которое может принимать double, чем между 1 и Infinity.

EDIT: да, результат mid == min, конечно же, является доказательством того, что предел двойной точности имеет был достигнут. Но инверсия mid! = Min не доказывает, что предел мог быть достигнут на другом этапе.

В общей программе, которая работает с произвольными входными двойниками, вам нужно будет выполнить такую ​​сортировку проверки с каждым промежуточным результатом расчета. Я думаю, что это не стоит усилий по сравнению с использованием BigDecimal, а также вы рискуете забыть о некоторых проверках.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...