По крайней мере, на Java, если я напишу этот код:
float a = 1000.0F;
float b = 0.00004F;
float c = a + b + b;
float d = b + b + a;
boolean e = c == d;
значение будет . Я полагаю, что это связано с тем, что поплавки очень ограничены в способе точного представления чисел. Но я не понимаю , почему просто изменить положение может вызвать это неравенство.
Я уменьшил значение s до 1 в обеих строках 3 и 4, как показано ниже, однако значение становится :
float a = 1000.0F;
float b = 0.00004F;
float c = a + b;
float d = b + a;
boolean e = c == d;
Что именно произошло в строках 3 и 4? Почему операции сложения с плавающей точкой не ассоциативны?
Заранее спасибо.
arithmetic
floating-point
numerical-algorithms
Известные Зеты
источник
источник
X
очень большое число иY
очень маленькое число, такое чтоX + Y = X
. ЗдесьX + Y + -X
будет ноль. НоX + -X + Y
будетY
.Ответы:
В типичных реализациях с плавающей запятой результат одной операции создается так, как если бы операция была выполнена с бесконечной точностью, а затем округляется до ближайшего числа с плавающей запятой.
Сравните и b + a : результат каждой операции, выполненной с бесконечной точностью, одинаков, поэтому эти идентичные результаты с бесконечной точностью округляются одинаковым образом. Другими словами, сложение с плавающей точкой коммутативно.а + б б + а
Возьмем : b - число с плавающей точкой. Для двоичных чисел с плавающей запятой 2 b также является числом с плавающей запятой (показатель степени на единицу больше), поэтому b + b добавляется без ошибок округления. Затем добавляется a к точному значению b + b . Результатом является точное значение 2 b + a , округленное до ближайшего числа с плавающей запятой.б + б + а б 2 б б + б a б + б 2 б + а
Возьмем : добавлено a + b , и будет ошибка округления r , поэтому мы получим результат a + b + r . Добавьте b , и в результате получите точное значение 2 b + a + r , округленное до ближайшего числа с плавающей запятой.а + б + б а + б р a + b + r б 2 б + а + р
Так в одном случае , округлые. В другом случае 2 b + a + r , округлено.2 б + а 2 б + а + р
PS. Для двух конкретных чисел и b оба вычисления дают один и тот же результат или нет, зависит от чисел и от ошибки округления в вычислениях a + b , и их обычно трудно предсказать. Использование одинарной или двойной точности в принципе не имеет значения для проблемы, но поскольку ошибки округления различны, будут значения a и b, где с одинарной точностью результаты равны, а с двойной точностью - нет, или наоборот. Точность будет намного выше, но проблема в том, что два выражения математически одинаковы, но не одинаковы в арифметике с плавающей точкой, остается неизменной.a б а + б
PPS. В некоторых языках арифметика с плавающей запятой может выполняться с более высокой точностью или с большим диапазоном чисел, чем задано фактическими утверждениями. В этом случае было бы гораздо более вероятно (но все еще не гарантировано), что обе суммы дают один и тот же результат.
PPPS. Комментарий спросил, должны ли мы спрашивать, равны ли числа с плавающей запятой или нет вообще. Абсолютно, если вы знаете, что делаете. Например, если вы сортируете массив или реализуете набор, вы попадаете в ужасные неприятности, если хотите использовать какое-то понятие «примерно равный». В графическом пользовательском интерфейсе вам может потребоваться пересчитать размеры объекта, если размер объекта изменился - вы сравниваете oldSize == newSize, чтобы избежать этого пересчета, зная, что на практике у вас почти никогда не бывает почти одинаковых размеров, и ваша программа верна даже если есть ненужный пересчет.
источник
b
в этом ответе не 0,00004, это то, что вы получите после конвертации и округления.Бинарный формат с плавающей запятой, поддерживаемый компьютерами, по сути аналогичен десятичной научной нотации, используемой людьми.
Число с плавающей точкой состоит из знака, мантиссы (фиксированная ширина) и показателя степени (фиксированная ширина), например:
Обычная научная запись имеет аналогичный формат:
Если мы выполняем арифметику в научной нотации с конечной точностью, округляя после каждой операции, то мы получим все те же плохие эффекты, что и двоичная с плавающей запятой.
пример
Для иллюстрации предположим, что мы используем ровно 3 цифры после десятичной точки.
(а + б) + б
Теперь мы вычисляем:
На следующем этапе, конечно:
Следовательно (a + b) + b = 9.999 × 10 4 .
(б + б) + а
Но если мы сделали операции в другом порядке:
Далее мы вычисляем:
Следовательно (b + b) + a = 1.000 × 10 5 , что отличается от нашего другого ответа.
источник
Java использует двоичное представление IEEE 754 с плавающей запятой, которое выделяет 23 двоичные цифры для мантиссы, которые нормализованы, чтобы начинаться с первой значащей цифры (опущено, чтобы сэкономить место).
Части в красном являются мантиссами, как они на самом деле представлены (до округления).
источник
Недавно мы столкнулись с похожей проблемой округления. Вышеупомянутые ответы являются правильными, но довольно техническими.
Я нашел следующее хорошее объяснение, почему существуют ошибки округления. http://csharpindepth.com/Articles/General/FloatingPoint.aspx
TLDR: двоичные числа с плавающей запятой не могут быть точно сопоставлены с десятичными числами с плавающей запятой. Это приводит к неточностям, которые могут возникнуть во время математических операций.
Пример, использующий десятичные числа с плавающей запятой: 1/3 + 1/3 + 1/3 обычно будет равен 1. Однако в десятичных числах: 0,333333 + 0,333333 + 0,333333 никогда не будет точно равен 1,000000.
То же самое происходит при выполнении математических операций над двоичными десятичными числами.
источник