Прежде всего, значения с плавающей запятой не являются «случайными» в своем поведении. Точное сравнение может иметь смысл в реальных ситуациях. Но если вы собираетесь использовать число с плавающей запятой, вам нужно знать, как оно работает. Ошибка в предположении, что числа с плавающей точкой работают как действительные числа, приведут к быстрому взлому кода. Ошибка в предположении, что с результатами с плавающей запятой связан большой случайный размытость (как предлагает большинство ответов здесь), и вы получите код, который сначала работает, но в конечном итоге приводит к ошибкам большой величины и ошибкам.
Прежде всего, если вы хотите программировать с плавающей запятой, вы должны прочитать это:
Что каждый компьютерщик должен знать об арифметике с плавающей точкой
Да, прочитайте все это. Если это слишком обременительно, вы должны использовать целые числа / фиксированную точку для своих расчетов, пока у вас не будет времени прочитать их. :-)
Теперь, с учетом сказанного, самые большие проблемы с точными сравнениями с плавающей точкой сводятся к:
Тот факт, что многие значения, которые вы можете записать в источнике или прочитать с помощью scanf
или strtod
, не существуют как значения с плавающей запятой и автоматически преобразуются в ближайшее приближение. Об этом говорил ответ demon9733.
Тот факт, что многие результаты округляются из-за отсутствия достаточной точности для представления фактического результата. Простой пример, где вы можете увидеть это добавление x = 0x1fffffe
и y = 1
плавание. Здесь он x
имеет 24 бита точности в мантиссе (хорошо) и y
имеет всего 1 бит, но когда вы добавляете их, их биты не находятся в перекрывающихся местах, и результат должен будет иметь 25 бит точности. Вместо этого он округляется ( 0x2000000
в режиме округления по умолчанию).
Тот факт, что многие результаты округляются из-за необходимости бесконечного количества мест для правильного значения. Это включает в себя как рациональные результаты, такие как 1/3 (с которым вы знакомы из десятичной дроби, где она занимает бесконечно много мест), так и 1/10 (которая также занимает бесконечно много мест в двоичной системе, поскольку 5 не является степенью 2), а также иррациональные результаты, такие как квадратный корень всего, что не является идеальным квадратом.
Двойное округление. В некоторых системах (в частности, в x86) выражения с плавающей запятой оцениваются с большей точностью, чем их номинальные типы. Это означает, что когда происходит один из указанных выше типов округления, вы получите два шага округления: сначала округление результата до типа с более высокой точностью, затем округление до конечного типа. В качестве примера рассмотрим, что происходит в десятичном виде, если округлить 1.49 до целого числа (1), а не в том, что происходит, если сначала округлить его до одного десятичного знака (1.5), а затем округлить полученный результат до целого числа (2). На самом деле это одна из самых неприятных областей в плавающей точке, поскольку поведение компилятора (особенно для глючных, не соответствующих стандарту компиляторов, таких как GCC) непредсказуемо.
Трансцендентные функции ( trig
, exp
, log
и т.д.) не определены , чтобы правильно округленные результаты; результат только что указан, чтобы быть правильным в пределах одной единицы в последнем месте точности (обычно упоминаемый как 1ulp ).
Когда вы пишете код с плавающей запятой, вам нужно помнить о том, что вы делаете с числами, которые могут привести к неточности результатов, и делать соответствующие сравнения. Часто имеет смысл сравнивать с «эпсилоном», но этот эпсилон должен основываться на величине сравниваемых чисел , а не на абсолютной константе. (В случаях, когда сработает абсолютная постоянная эпсилон, это сильно указывает на то, что фиксированная точка, а не с плавающей точкой, является правильным инструментом для работы!)
Изменить: В частности, проверка эпсилон-относительной величины должна выглядеть примерно так:
if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))
Где FLT_EPSILON
находится константа float.h
(замените ее DBL_EPSILON
на double
s или LDBL_EPSILON
для long double
s), и K
вы выбираете такую константу, чтобы накопленная ошибка ваших вычислений была определенно ограничена K
единицами в последнем месте (и если вы не уверены, что получили ошибку связанный расчет правильно, сделайте K
в несколько раз больше, чем ваши вычисления говорят, что это должно быть).
Наконец, обратите внимание, что если вы используете это, может потребоваться некоторая особая осторожность вблизи нуля, так FLT_EPSILON
как не имеет смысла для ненормированных. Быстрое решение было бы сделать это:
if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)
и аналогично заменить, DBL_MIN
если использовать удваивается.
fabs(x+y)
проблематично, еслиx
иy
(может) иметь другой знак. Тем не менее, хороший ответ против потока культовых сравнений.