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

152

Я знаю, что 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что можно точно представить?

icza
источник
7
Если вы действительно исследуете это, я уверен, что вы можете понять это, но с плавающей точкой загружены «сюрпризы», и иногда лучше просто смотреть в изумлении.
Hot Licks
3
Вы думаете об этом в математике. Арифметика с плавающей точкой ни в коем случае не математика.
Якоб
13
@ Хотельк, это очень неправильное отношение.
Хоббс
2
@RussellBorogove, даже если бы она была оптимизирована, она была бы действительной оптимизацией, только если бы она sumимела такое же конечное значение, как если бы цикл действительно выполнялся. В стандарте C ++ это называется «как если бы правило» или «такое же наблюдаемое поведение».
Хоббс
7
@ Якоб совсем не правда. Арифметика с плавающей точкой строго определена, с хорошей математической обработкой границ ошибок и тому подобного. Просто многие программисты либо не хотят выполнять анализ, либо ошибочно полагают, что «с плавающей запятой неточно» - это все, что нужно знать, и что анализ не стоит беспокоиться.
Хоббс

Ответы:

155

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

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

Питер Лори
источник
1
Где это реализовано?
EpicPandaForce
16
@Zhuinden Процессор соответствует стандарту IEEE-754. Java дает вам доступ к базовым инструкциям процессора и не вмешивается. en.wikipedia.org/wiki/IEEE_floating_point
Питер Лоури,
10
@PeterLawrey: не обязательно процессор. На машине без плавающей запятой в CPU (и без использования отдельного FPU) арифметика IEEE будет выполняться программным обеспечением. И если у центрального процессора есть плавающая точка, но он не соответствует требованиям IEEE, я думаю, что реализация Java для этого процессора также будет вынуждена использовать мягкое плавание ...
R .. GitHub STOP HELPING ICE
1
@R .. в этом случае я не знаю, что случилось бы, если бы вы использовали strictfp Time для рассмотрения целых чисел с фиксированной точкой, я думаю. (или BigDecimal)
Питер Лоури
2
@eugene ключевая проблема - ограниченные значения, которые может представлять плавающая точка. Это ограничение может привести к потере информации, а по мере увеличения числа ошибок. Он использует округление, но в этом случае округление в меньшую сторону, так что число, которое было бы немного слишком большим, так как 0,1 немного больше, превращается в правильное значение. Точно 0,5
Питер Лоури
47

Переполнение запрета в плавающей запятой 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находится в той же бинаде, что и 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

Паскаль Куок
источник
1
В первой строке вы говорите, что x + x + x является абсолютно правильным, но из примера в вопросе это не так.
Альбоз
2
@Alboz Я говорю, что x + x + xэто точно правильно округленное число с плавающей запятой до реального 3 * x. «Правильно округленный» означает «ближайший» в этом контексте.
Паскаль Куок
4
+1 Это должен быть принятый ответ. Это на самом деле предлагает объяснения / доказательства того, что происходит, а не просто смутные обобщения.
R .. GitHub ОСТАНОВИТЬ ЛЬДА
1
@Alboz (все это предусмотрено вопросом). Но этот ответ объясняет, как ошибки случайно удаляются, а не складываются в худшем случае.
Хоббс
1
@chebus 0.1 - это 0x1.999999999999999999999… p-4 в шестнадцатеричном формате (бесконечная последовательность цифр). Он округляется с двойной точностью как 0x1.99999ap-4. 0.2 - это 0x1.999999999999999999999… p-3 в шестнадцатеричном формате. По той же причине, что 0,1 приблизительно равно 0x1,999999ap-4, 0,2 приблизительно равно 0x1,99999ap-3. Между тем, 0x1.99999ap-3 - это точно 0x1.99999ap-4 + 0x1.99999ap-4.
Паскаль Куок
-1

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

Думайте о плавающей точке как о замечательном, но НЕПРАВИЛЬНОМ способе представлять числа Не все возможные числа легко представлены в компьютере. Иррациональные числа типа Пи. Или как SQRT (2). (Символические математические системы могут представлять их, но я сказал «легко».)

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

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

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

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

Для подсчета заявок (в том числе бухгалтерских) используйте целое число. Для подсчета количества людей, проходящих через ворота, используйте int или long.

DannyB
источник
2
Вопрос помечен как [java]. В определении языка Java не предусмотрено «несколько дополнительных битов точности», только для нескольких дополнительных битов экспоненты (и это только если вы не используете strictfp). То, что вы отказались понимать что-то, не означает, что это непостижимо, и что другие не должны отказываться, чтобы понять это. См. Stackoverflow.com/questions/18496560 в качестве примера длин, на которые пойдут реализации Java для реализации определения языка (которое не включает каких-либо положений для битов дополнительной точности, а также strictfpдля любого дополнительного бита exp)
Pascal Cuoq