Неравенство, вызванное неточностью поплавка

15

По крайней мере, на Java, если я напишу этот код:

float a = 1000.0F;
float b = 0.00004F;
float c = a + b + b;
float d = b + b + a;
boolean e = c == d;

значение будет . Я полагаю, что это связано с тем, что поплавки очень ограничены в способе точного представления чисел. Но я не понимаю , почему просто изменить положение может вызвать это неравенство.efalsea

Я уменьшил значение s до 1 в обеих строках 3 и 4, как показано ниже, однако значение становится :betrue

float a = 1000.0F;
float b = 0.00004F;
float c = a + b;
float d = b + a;
boolean e = c == d;

Что именно произошло в строках 3 и 4? Почему операции сложения с плавающей точкой не ассоциативны?

Заранее спасибо.

Известные Зеты
источник
16
Как показывает ваш пример, сложение с плавающей точкой является коммутативным. Но это не ассоциативно.
Юваль Фильмус
1
Я призываю вас посмотреть основные определения. Также обратите внимание, что компилятор анализирует как ( r + s ) + t (сложение связано слева). р+s+T(р+s)+T
Юваль Фильмус
2
Чтобы легко понять, почему это так, рассмотрим Xочень большое число и Yочень маленькое число, такое что X + Y = X. Здесь X + Y + -Xбудет ноль. Но X + -X + Yбудет Y.
Дэвид Шварц

Ответы:

20

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

Сравните и b + a : результат каждой операции, выполненной с бесконечной точностью, одинаков, поэтому эти идентичные результаты с бесконечной точностью округляются одинаковым образом. Другими словами, сложение с плавающей точкой коммутативно.a+бб+a

Возьмем : b - число с плавающей точкой. Для двоичных чисел с плавающей запятой 2 b также является числом с плавающей запятой (показатель степени на единицу больше), поэтому b + b добавляется без ошибок округления. Затем добавляется a к точному значению b + b . Результатом является точное значение 2 b + a , округленное до ближайшего числа с плавающей запятой.б+б+aб2бб+бaб+б2б+a

Возьмем : добавлено a + b , и будет ошибка округления r , поэтому мы получим результат a + b + r . Добавьте b , и в результате получите точное значение 2 b + a + r , округленное до ближайшего числа с плавающей запятой.a+б+бa+брa+б+рб2б+a+р

Так в одном случае , округлые. В другом случае 2 b + a + r , округлено.2б+a2б+a+р

PS. Для двух конкретных чисел и b оба вычисления дают один и тот же результат или нет, зависит от чисел и от ошибки округления в вычислениях a + b , и их обычно трудно предсказать. Использование одинарной или двойной точности в принципе не имеет значения для проблемы, но поскольку ошибки округления различны, будут значения a и b, где с одинарной точностью результаты равны, а с двойной точностью - нет, или наоборот. Точность будет намного выше, но проблема в том, что два выражения математически одинаковы, но не одинаковы в арифметике с плавающей точкой, остается неизменной.aбa+б

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

PPPS. Комментарий спросил, должны ли мы спрашивать, равны ли числа с плавающей запятой или нет вообще. Абсолютно, если вы знаете, что делаете. Например, если вы сортируете массив или реализуете набор, вы попадаете в ужасные неприятности, если хотите использовать какое-то понятие «примерно равный». В графическом пользовательском интерфейсе вам может потребоваться пересчитать размеры объекта, если размер объекта изменился - вы сравниваете oldSize == newSize, чтобы избежать этого пересчета, зная, что на практике у вас почти никогда не бывает почти одинаковых размеров, и ваша программа верна даже если есть ненужный пересчет.

gnasher729
источник
В этом конкретном случае b становится периодическим при преобразовании в двоичный код, поэтому ошибки округления везде.
Андре Соуза Лемос
1
@ AndréSouzaLemos bв этом ответе не 0,00004, это то, что вы получите после конвертации и округления.
Алексей Романов
«В типичных реализациях с плавающей запятой результат одной операции создается так, как если бы операция выполнялась с бесконечной точностью, а затем округлялась до ближайшего числа с плавающей запятой», - на самом деле это требуется спецификацией, к моему большому сожалению. когда я попытался реализовать это с точки зрения логических элементов (симулятор мог обрабатывать только 64-битные шины).
Джон Дворак
Наивный вопрос: имеет ли смысл когда-либо проверять равенство с плавающей точкой? Почему большинство языков программирования допускают тест aa == b, где оба или один являются плавающими?
curious_cat
Соответствующее определение из Википедии: « Машина Эпсилон дает верхнюю границу относительной ошибки из-за округления в арифметике с плавающей запятой».
Блэкхок
5

Бинарный формат с плавающей запятой, поддерживаемый компьютерами, по сути аналогичен десятичной научной нотации, используемой людьми.

Число с плавающей точкой состоит из знака, мантиссы (фиксированная ширина) и показателя степени (фиксированная ширина), например:

+/-  1.0101010101 × 2^12345
sign   ^mantissa^     ^exp^

Обычная научная запись имеет аналогичный формат:

+/- 1.23456 × 10^99

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


пример

Для иллюстрации предположим, что мы используем ровно 3 цифры после десятичной точки.

a = 99990 = 9.999 × 10^4
b =     3 = 3.000 × 10^0

(а + б) + б

Теперь мы вычисляем:

c = a + b
  = 99990 + 3      (exact)
  = 99993          (exact)
  = 9.9993 × 10^4  (exact)
  = 9.999 × 10^4.  (rounded to nearest)

На следующем этапе, конечно:

d = c + b
  = 99990 + 3 = ...
  = 9.999 × 10^4.  (rounded to nearest)

Следовательно (a + b) + b = 9.999 × 10 4 .

(б + б) + а

Но если мы сделали операции в другом порядке:

e = b + b
  = 3 + 3  (exact)
  = 6      (exact)
  = 6.000 × 10^0.  (rounded to nearest)

Далее мы вычисляем:

f = e + a
  = 6 + 99990      (exact)
  = 99996          (exact)
  = 9.9996 × 10^4  (exact)
  = 1.000 × 10^5.  (rounded to nearest)

Следовательно (b + b) + a = 1.000 × 10 5 , что отличается от нашего другого ответа.

Nayuki
источник
5

Java использует двоичное представление IEEE 754 с плавающей запятой, которое выделяет 23 двоичные цифры для мантиссы, которые нормализованы, чтобы начинаться с первой значащей цифры (опущено, чтобы сэкономить место).

0,0000410знак равно0,00000000000000101001111100010110101100010001110001101101000111 ...2знак равно[1.]+01001111100010110101100010001110001101101000111 ...2×2-15

100010+0,0000410знак равно1111101000,00000000000000101001111100010110101100010001110001101101000111 ...2знак равно[1.]+11110100000000000000000101001111100010110101100010001110001101101000111 ...2×29

Части в красном являются мантиссами, как они на самом деле представлены (до округления).

(100010+0,0000410)+0,0000410(0,0000410+0,0000410)+100010

Андре Соуза Лемос
источник
0

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

Я нашел следующее хорошее объяснение, почему существуют ошибки округления. http://csharpindepth.com/Articles/General/FloatingPoint.aspx

TLDR: двоичные числа с плавающей запятой не могут быть точно сопоставлены с десятичными числами с плавающей запятой. Это приводит к неточностям, которые могут возникнуть во время математических операций.

Пример, использующий десятичные числа с плавающей запятой: 1/3 + 1/3 + 1/3 обычно будет равен 1. Однако в десятичных числах: 0,333333 + 0,333333 + 0,333333 никогда не будет точно равен 1,000000.

То же самое происходит при выполнении математических операций над двоичными десятичными числами.

Фрик Сандерс
источник