Прежде всего, значения с плавающей запятой не являются «случайными» в своем поведении. Точное сравнение может иметь смысл в реальных ситуациях. Но если вы собираетесь использовать число с плавающей запятой, вам нужно знать, как оно работает. Ошибка в предположении, что числа с плавающей точкой работают как действительные числа, приведут к быстрому взлому кода. Ошибка в предположении, что с результатами с плавающей запятой связан большой случайный размытость (как предлагает большинство ответов здесь), и вы получите код, который сначала работает, но в конечном итоге приводит к ошибкам большой величины и ошибкам.
Прежде всего, если вы хотите программировать с плавающей запятой, вы должны прочитать это:
Что каждый компьютерщик должен знать об арифметике с плавающей точкой
Да, прочитайте все это. Если это слишком обременительно, вы должны использовать целые числа / фиксированную точку для своих расчетов, пока у вас не будет времени прочитать их. :-)
Теперь, с учетом сказанного, самые большие проблемы с точными сравнениями с плавающей точкой сводятся к:
Тот факт, что многие значения, которые вы можете записать в источнике или прочитать с помощью 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на doubles или LDBL_EPSILONдля long doubles), и 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(может) иметь другой знак. Тем не менее, хороший ответ против потока культовых сравнений.