Почему добавление 0,1 несколько раз остается без потерь?

Я знаю, что десятичное число 0.1 не может быть представлено точно с конечным двоичным числом (объяснение), поэтому double n = 0.1 потеряет некоторую точность и не будет точно 0.1. С другой стороны, 0.5 можно представить точно, потому что это 0.5 = 1/2 = 0.1b.

Сказав, что понятно, что добавление 0.1 три раза не даст точно 0.3, поэтому следующий код печатает false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Но как получается, что добавление 0.1 пять раз даст ровно 0.5? Следующий код печатает true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Если 0.1 не может быть представлено точно, как это сделать, добавив его 5 раз, дает ровно 0.5, который может быть представлен точно?

Ответ 1

Ошибка округления не является случайной и, как она реализована, она пытается минимизировать ошибку. Это означает, что иногда ошибка не видна, или нет ошибки.

Например 0.1 не точно 0.1 i.e. new BigDecimal("0.1") < new BigDecimal(0.1), но 0.5 точно 1.0/2

Эта программа показывает действительные значения.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

печатает

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Обратите внимание: что 0.3 немного выключен, но когда вы добираетесь до 0.4, бит должен сдвигаться на один, чтобы вписаться в 53-битный предел, и ошибка отбрасывается. Опять же, ошибка закрадывается назад для 0.6 и 0.7, но для 0.8 - 1.0 ошибка отбрасывается.

Добавление 5 раз должно накапливать ошибку, а не отменять ее.

Причина возникновения ошибки связана с ограниченной точностью. т.е. 53 бит. Это означает, что, поскольку число использует больше бит по мере того, как оно становится больше, бит должен быть сброшен с конца. Это вызывает округление, которое в этом случае в вашу пользу.
Вы можете получить противоположный эффект при получении меньшего числа, например. 0.1-0.0999 = > 1.0000000000000286E-4  и вы видите больше ошибок, чем раньше.

Примером этого является то, почему в Java 6 Почему Math.round(0.49999999999999994) возвращает 1 В этом случае потеря бит в вычислении приводит к большому разница с ответом.

Ответ 2

Запрет переполнения в плавающей запятой x + x + x - это точно округленное (то есть ближайшее) число с плавающей запятой до реального 3 * x, x + x + x + x составляет ровно 4 * x и x + x + x + x + x снова является правильно округленным с плавающей точкой приближением для 5 * x.

Первый результат для x + x + x вытекает из того, что x + x является точным. x + x + x является результатом только одного округления.

Второй результат сложнее, одна демонстрация его обсуждается здесь (и Стивен Канон ссылается на другое доказательство анализа случая на последние 3 цифры x). Подводя итог, либо 3 * x находится в том же binade, что 2 * x, либо он находится в том же бинаде, что и 4 * x, и в каждом случае можно сделать вывод, что ошибка в третьем добавлении отменяет ошибку при втором добавлении (первое добавление является точным, как мы уже говорили).

Третий результат: "x + x + x + x + x правильно округлен" , происходит от второго таким же образом, что и первый результат от точности x + x.


Второй результат объясняет, почему 0.1 + 0.1 + 0.1 + 0.1 - это точно число с плавающей запятой 0.4: рациональные числа 1/10 и 4/10 приближаются одинаково, с той же относительной ошибкой, когда они преобразуются в плавающие точки, Эти числа с плавающей запятой имеют отношение ровно 4 между ними. Первый и третий результат показывают, что 0.1 + 0.1 + 0.1 и 0.1 + 0.1 + 0.1 + 0.1 + 0.1 могут иметь меньше ошибок, чем можно было бы предположить путем анализа наивных ошибок, но сами по себе они связывают результаты только с 3 * 0.1 и 5 * 0.1, который можно ожидать близким, но не обязательно идентичным 0.3 и 0.5.

Если вы добавите 0.1 после четвертого добавления, вы, наконец, заметите ошибки округления, которые добавят "0.1 добавлено к себе n раз" , отклонившись от n * 0.1 и расходясь еще больше от n/10. Если бы вы планировали значение "0,1 добавлено к себе n раз" как функция n, вы бы наблюдали линии постоянного наклона по бинадам (как только результат n-го добавления суждено попасть в конкретный бинад, можно ожидать, что свойства добавления будут похожи на предыдущие дополнения, которые дали результат в том же бинаде). Внутри одного бинада ошибка будет либо расти, либо уменьшаться. Если вы посмотрите на последовательность наклонов от бинада до бинада, вы будете распознавать повторяющиеся цифры 0.1 в двоичном формате некоторое время. После этого поглощение начнет происходить, и кривая будет плоской.

Ответ 3

Системы с плавающей запятой выполняют различную магию, включая несколько дополнительных бит точности для округления. Таким образом, очень малая ошибка из-за неточного представления 0,1 заканчивается округлением до 0,5.

Думайте о плавающей точке как о отличном, но INEXACT способе представления чисел. Не все возможные номера легко представлены на компьютере. Иррациональные числа, такие как PI. Или как SQRT (2). (Символьные математические системы могут представлять их, но я сказал "легко".)

Значение с плавающей запятой может быть очень близким, но не точным. Это может быть так близко, что вы можете перейти к Плутону и оторваться на миллиметры. Но все же не точный в математическом смысле.

Не используйте с плавающей точкой, когда вам нужно быть точным, а не приближенным. Например, учетные приложения хотят точно отслеживать определенное количество копеек в учетной записи. Целые являются хорошими для этого, потому что они точны. Основной проблемой, с которой вам нужно следить за целыми числами, является переполнение.

Использование BigDecimal для валюты работает хорошо, потому что базовое представление представляет собой целое число, хотя и большое.

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

Для подсчета приложений (включая учетные записи) используйте integer. Для подсчета количества людей, которые проходят через ворота, используйте int или long.