Является ли плавающая точка == когда-либо ОК?
Только сегодня я столкнулся со сторонним программным обеспечением, которое мы используем, и в их образце кода было что-то вроде этих строк:
// Defined in somewhere.h static const double BAR = 3.14; // Code elsewhere.cpp void foo(double d) { if (d == BAR) ... }
Я знаю о проблеме с плавающей точкой и их представлении, но мне стало интересно, есть ли случаи, когда float == float
будет в порядке? Я не прошу, когда это может сработать, но когда это имеет смысл и работает.
Кроме того, как насчет вызова типа foo(BAR)
? Будет ли это всегда сравнивать равным, поскольку оба они используют один и тот же static const BAR
?
- Разница в арифметике с плавающей запятой между x86 и x64
- Проблемы сравнения с плавающей запятой MySQL
- Почему мы не можем использовать '==' для сравнения двух чисел с плавающей запятой или двойных чисел
- извлечение мантиссы и экспонента из двойного в c #
- Каким детерминированным является неточность с плавающей запятой?
- Как компьютер выполняет арифметику с плавающей запятой?
- Как разобрать строку на float или int в Python?
- Сравнение с плавающей точкой
- Двойной расчет, создающий нечетный результат
- double или float, что быстрее?
- Как преобразовать строку в float?
- Преобразование float в double без потери точности
- Почему целочисленное деление на -1 (отрицательное) приводит к FPE?
На этот вопрос можно ответить двумя способами:
- Существуют ли случаи, когда
float == float
дает правильный результат? - Существуют ли случаи, когда
float == float
является приемлемым кодированием?
Ответ на (1): Да, иногда. Но это будет хрупким, что приводит к ответу на (2): Нет. Не делайте этого. Вы просите о причудливых ошибках в будущем.
Что касается вызова формы foo(BAR)
: в этом конкретном случае сравнение вернет true, но когда вы пишете foo
вы не знаете (и не должны зависеть) от того, как он называется. Например, вызов foo(BAR)
будет прекрасным, но foo(BAR * 2.0 / 2.0)
(или даже, может быть, foo(BAR * 1.0)
зависимости от того, насколько компилятор оптимизирует вещи) сломается. Вы не должны полагаться на вызывающего, не выполняющего арифметики!
Короче говоря, хотя a == b
будет работать в некоторых случаях, вы действительно не должны полагаться на него. Даже если вы можете гарантировать семантику вызова сегодня, возможно, вы не сможете гарантировать их на следующей неделе, чтобы избавить себя от боли и не использовать ==
.
На мой взгляд, float == float
никогда не будет * ОК, потому что это почти невозможно.
* При малых значениях никогда.
Да, вам гарантировано, что целые числа, включая 0.0, сравниваются с ==
Конечно, вы должны быть немного осторожны с тем, как вы получили все число в первую очередь, назначение безопасно, но результат любого расчета является подозрительным
ps есть набор действительных чисел, которые имеют идеальное воспроизведение как плавающий (думаю, 1/2, 1/4 1/8 и т. д.), но вы, вероятно, заранее не знаете, что у вас есть один из них.
Просто для уточнения. IEEE 754 гарантируется, что плавающие представления целых чисел (целые числа) в пределах диапазона являются точными.
float a=1.0; float b=1.0; a==b // true
Но вы должны быть осторожны, как вы получаете целые числа
float a=1.0/3.0; a*3.0 == 1.0 // not true !!
Другие ответы объясняют, что использование чисел ==
для чисел с плавающей запятой опасно. Я просто нашел один пример, который хорошо иллюстрирует эти опасности.
На платформе x86 вы можете получить странные результаты с плавающей запятой для некоторых вычислений, которые не связаны с проблемами округления, присущими выполняемым вычислениям. Эта простая программа C иногда печатает «ошибку»:
#include void test(double x, double y) { const double y2 = x + 1.0; if (y != y2) printf("error\n"); } void main() { const double x = .012; const double y = x + 1.0; test(x, y); }
Программа по существу просто вычисляет
x = 0.012 + 1.0; y = 0.012 + 1.0;
(распространяется только на две функции и с промежуточными переменными), но сравнение все равно может дать false!
Причина в том, что на платформе x86 программы обычно используют x90 FPU для вычислений с плавающей запятой. X87 внутренне вычисляет с более высокой точностью, чем обычный double
, поэтому double
значения необходимо округлять, когда они хранятся в памяти. Это означает, что кругооборот x87 -> ОЗУ -> x87 теряет точность, и, следовательно, результаты вычислений различаются в зависимости от того, прошли ли промежуточные результаты через ОЗУ или все они остались в регистрах FPU. Это, конечно, решение для компилятора, поэтому ошибка отображается только для определенных компиляторов и настроек оптимизации :-(.
Подробнее см. Ошибку GCC: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=323
Скорее страшно …
Дополнительное примечание:
Ошибки такого типа, как правило, довольно сложно отлаживать, потому что разные значения становятся одинаковыми, когда они попадают в ОЗУ.
Поэтому, если вы, например, продлеваете вышеуказанную программу, чтобы фактически распечатать битовые шаблоны y
и y2
сразу после их сравнения, вы получите то же самое значение . Чтобы распечатать значение, его необходимо загрузить в ОЗУ, чтобы его можно было передать какой-либо функции печати, такой как printf
, и это приведет к исчезновению различий …
Идеально подходит для интегральных значений даже в форматах с плавающей запятой
Но короткий ответ: «Нет, не используйте ==.»
Как ни странно, формат с плавающей запятой работает «отлично», т. Е. С точной точностью, при работе с интегральными значениями в диапазоне формата. Это означает, что если вы придерживаетесь двойных значений, вы получите отличные целые числа с чуть более 50 бит, что даст вам около + 4,500,000,000,000,000 или 4,5 квадриллиона.
Фактически, именно так работает JavaScript внутри, и поэтому JavaScript может делать такие вещи, как +
и -
на действительно больших числах, но может только <<
и >>
на 32-битных.
Строго говоря, вы можете точно сравнить суммы и произведения чисел с точными представлениями. Это были бы целые числа, плюс фракции, состоящие из 1/2 n терминов. Таким образом, цикл, увеличивающий на n + 0,25, n + 0,50 или n + 0,75 , будет штрафом, но не любой из других 96 десятичных дробей с 2 цифрами.
Таким образом, ответ таков: хотя точное определение может теоретически иметь смысл в узких случаях, его лучше избегать.
Единственный случай, когда я когда-либо использовал ==
(или !=
) Для float, заключается в следующем:
if (x != x) { // Here x is guaranteed to be Not a Number }
и я должен признать, что я виновен в использовании Not A Number в качестве волшебной константы с плавающей запятой (с использованием numeric_limits
в C ++).
Нет смысла сравнивать числа с плавающей точкой для строгого равенства. Числа с плавающей запятой были разработаны с предсказуемыми пределами относительной точности. Вы несете ответственность за то, какую точность следует ожидать от них и от ваших алгоритмов.
Я постараюсь предоставить более или менее реальный пример законного, содержательного и полезного тестирования для равномерного распределения.
#include #include /* let's try to numerically solve a simple equation F(x)=0 */ double F(double x) { return 2*cos(x) - pow(1.2, x); } /* I'll use a well-known, simple&slow but extremely smart method to do this */ double bisection(double range_start, double range_end) { double a = range_start; double d = range_end - range_start; int counter = 0; while(a != a+d) // <-- WHOA!! { d /= 2.0; if(F(a)*F(a+d) > 0) /* test for same sign */ a = a+d; ++counter; } printf("%d iterations done\n", counter); return a; } int main() { /* we must be sure that the root can be found in [0.0, 2.0] */ printf("F(0.0)=%.17f, F(2.0)=%.17f\n", F(0.0), F(2.0)); double x = bisection(0.0, 2.0); printf("the root is near %.17f, F(%.17f)=%.17f\n", x, x, F(x)); }
Я бы предпочел не объяснять используемый метод биссекции , но подчеркнуть условие остановки. Он имеет точно обсуждаемую форму: (a == a+d)
где обе стороны являются поплавками: a
– наше текущее приближение корня уравнения, d
– наша текущая точность. Учитывая предварительное условие алгоритма – что между range_start
и range_end
должен быть корень, мы гарантируем на каждой итерации, что корень остается между a
и a+d
а d
уменьшается на два этапа на каждом шаге, уменьшая границы.
И затем, после ряда итераций, d
становится настолько маленьким, что во время добавления с ним он округляется до нуля! То есть a+d
оказывается ближе к a
затем к любому другому поплавку ; и поэтому FPU округляет его до ближайшего значения: к самому а. Это можно легко проиллюстрировать вычислением на гипотетической вычислительной машине; пусть у него есть 4-значная десятичная мантисса и некоторый большой диапазон экспонентов. Тогда какой результат машина должна дать 2.131e+02 + 7.000e-3
? Точный ответ – 213.107
, но наша машина не может представлять такое число; он должен округлить его. И 213.107
намного ближе к 213.1
чем к 213.2
– поэтому округленный результат становится 2.131e+02
– небольшое слагаемое исчезает, округляется до нуля. Точно так же гарантировано произойдет на некоторой итерации нашего алгоритма – и в этот момент мы больше не сможем продолжить. Мы нашли корень максимально возможной точности.
Назидательный вывод, по-видимому, заключается в том, что плавать сложно. Они выглядят так же, как реальные цифры, что каждый программист испытывает соблазн думать о них как о реальных числах. Но это не так. У них свое поведение, немного напоминающее реальное , но не совсем то же самое. Вы должны быть очень осторожны с ними, особенно при сравнении для равенства.
Обновить
Повторяя ответ через некоторое время, я также заметил интересный факт: в вышеприведенном алгоритме нельзя фактически использовать «небольшое количество» в состоянии остановки. Для любого выбора числа будут введены входы, которые сочтут ваш выбор слишком большим , что приведет к потере точности, и будут введены входы, которые сочтут ваш выбор слишком малым , вызывая избыточные итерации или даже входя в бесконечный цикл. Далее следует подробное обсуждение.
Возможно, вы уже знаете, что в исчислении нет понятия «маленькое число»: для любого действительного числа вы можете легко найти бесконечно много даже более мелких. Проблема в том, что одним из тех «еще меньших» может быть то, что мы действительно ищем; это может быть корень нашего уравнения. Хуже того, для разных уравнений могут быть разные корни (например, 2.51e-8
и 1.38e-8
), оба из которых будут приближаться к тому же числу, если наше условие остановки будет выглядеть как d < 1e-6
. Независимо от того, какое «небольшое число» вы выберете, многие корни, которые были бы правильно найдены с максимальной точностью с условием остановки a == a+d
будут испорчены слишком большим «эпсилон».
Однако верно, что в числах с плавающей запятой экспонента имеет ограниченный диапазон, поэтому вы можете найти наименьшее ненулевое положительное число FP (например, деноминация 1e-45
для одноточечной точности IEEE 754). Но это бесполезно! while (d < 1e-45) {...}
будет зацикливаться навсегда, предполагая одноточную (положительную отличную от нуля) d
.
Исходя из этих случаев патологического края, любой выбор «малого числа» в состоянии остановки d < eps
будет слишком мал для многих уравнений. В тех уравнениях, где корень имеет показатель достаточно высокий, результат вычитания двух мантисс, отличающихся только на младшую значащую цифру, будет легко превышать наш «эпсилон». Например, с 6-значными мантиссами 7.00023e+8 - 7.00022e+8 = 0.00001e+8 = 1.00000e+3 = 1000
, что означает, что наименьшая возможная разница между числами с показателем +8 и 5-значной мантиссой. 1000! Который никогда не впишется, скажем, в 1e-4
. Для этих чисел с (относительно) высоким показателем мы просто не имеем достаточной точности, чтобы видеть разницу 1e-4
.
Моя реализация выше также учитывала эту последнюю проблему, и вы можете видеть, что d
уменьшается на два этапа каждый раз, вместо того, чтобы пересчитываться как разница (возможно, огромная по показателю) a
и b
. Поэтому, если мы изменим условие остановки на d < eps
, алгоритм не будет застревать в бесконечном цикле с огромными корнями (он очень хорошо мог бы с (ba) < eps
), но все равно будет выполнять ненужные итерации во время сокращения d
ниже точность.
Такое рассуждение может показаться слишком теоретическим и излишне глубоким, но его цель - снова проиллюстрировать хитрость поплавков. Нужно быть очень осторожным в отношении их конечной точности при написании вокруг них арифметических операторов.
Возможно, это нормально, если вы никогда не будете вычислять значение, прежде чем сравнивать его. Если вы проверяете, является ли число с плавающей запятой точно pi или -1 или 1, и вы знаете, что это ограниченные значения, передаваемые в …
Я также использовал его несколько раз, переписывая несколько алгоритмов в многопоточные версии. Я использовал тест, сравнивающий результаты для одно- и многопоточной версии, чтобы убедиться, что оба они дают точно такой же результат.
Да. 1/x
будет действительным, если x==0
. Здесь вам не нужен неточный тест. 1/0.00000001
отлично. Я не могу придумать другого случая – вы даже не можете проверить tan(x)
на x==PI/2
Предположим, у вас есть функция, которая масштабирует массив поплавков постоянным фактором:
void scale(float factor, float *vector, int extent) { int i; for (i = 0; i < extent; ++i) { vector[i] *= factor; } }
Я предполагаю, что ваша реализация с плавающей точкой может точно представлять 1.0 и 0.0, а 0.0 представляется всеми 0 битами.
Если factor
равен 1.0, то эта функция не работает, и вы можете вернуться без какой-либо работы. Если factor
равен 0.0, то это может быть реализовано с вызовом memset, который, вероятно, будет быстрее, чем выполнение умножения с плавающей запятой индивидуально.
Эталонная реализация функций BLAS в netlib широко использует такие методы.
Другие сообщения показывают, где это уместно. Я думаю, что использование бит-точных сравнений, чтобы избежать ненужных вычислений, также хорошо.
Пример:
float someFunction (float argument) { // I really want bit-exact comparison here! if (argument != lastargument) { lastargument = argument; cachedValue = very_expensive_calculation (argument); } return cachedValue; }
На мой взгляд, сравнение для равенства (или некоторой эквивалентности) является требованием в большинстве ситуаций: стандартные контейнеры C ++ или алгоритмы с подразумеваемым функтором сравнения равенства, например std :: unordered_set, требуют, чтобы этот компаратор был отношением эквивалентности (см. C ++ именованные требования: UnorderedAssociativeContainer ).
К сожалению, сравнение с эпсилон, как и в abs(a - b) < epsilon
, не дает отношения эквивалентности, поскольку оно теряет транзитивность. Это, скорее всего, неопределенное поведение, в частности два «почти равных» числа с плавающей запятой могут давать разные hashи; это может привести к тому, что unordered_set окажется недопустимым. Лично я использую == для плавающих точек большую часть времени, если только какие-либо вычисления FPU не будут задействованы в каких-либо операндах. С контейнерами и контейнерными алгоритмами, где задействуются только чтение / запись, == (или любое отношение эквивалентности) является самым безопасным.
abs(a - b) < epsilon
является более или менее критерием сходимости, аналогичным пределу. Я считаю это отношение полезным, если мне нужно проверить, что математическое соответствие выполняется между двумя вычислениями (например, PV = nRT или distance = time * speed).
Короче говоря, используйте ==
если и только если вычисление с плавающей точкой не происходит; никогда не используйте abs(ab) < e
как предикат равенства;
Я бы сказал, что сравнение float для равенства было бы ОК, если ложно-отрицательный ответ приемлем .
Предположим, например, что у вас есть программа, которая выводит значения с плавающей запятой на экран и что, если значение с плавающей запятой окажется в точности равным M_PI
, вы хотите, чтобы он распечатывал «pi». Если значение отклоняется от небольшого бита от точного двойного представления M_PI
, оно будет печатать вместо него двойное значение, которое является одинаково допустимым, но немного менее читаемым для пользователя.
У меня есть программа рисования, которая принципиально использует плавучую точку для своей системы координат, так как пользователю разрешено работать с любой степенью детализации / масштабирования. То, что они рисуют, содержит строки, которые могут быть согнуты в созданных ими точках. Когда они перетаскивают одну точку поверх другой, они объединяются.
Чтобы сделать «правильное» сравнение с плавающей запятой, мне пришлось бы придумать некоторый диапазон, в котором можно было бы считать точки одинаковыми. Поскольку пользователь может увеличивать масштаб до бесконечности и работать в этом диапазоне, и поскольку я не мог заставить кого-либо совершить какой-либо диапазон, мы просто используем ‘==’, чтобы увидеть, совпадают ли точки. Иногда возникает проблема, когда точки, которые должны быть точно такими же, отключены на .000000000001 или что-то (особенно около 0,0), но обычно это работает нормально. Похоже, что сложно объединить точки без включенной привязки … или, по крайней мере, так работала исходная версия.
Иногда это бросает группу тестирования, но это их проблема: p
Так или иначе, есть пример, возможно, разумное время для использования ‘==’. Следует отметить, что решение меньше о технической точности, чем о пожеланиях клиента (или их отсутствии) и удобстве. В любом случае, это не то, что нужно. Итак, что, если два момента не сольются, когда вы их ожидаете? Это не конец света и не будет влиять на «расчеты».