Равномерное равенство
Общеизвестно, что при сравнении значений с плавающей запятой нужно быть осторожным. Обычно вместо использования ==
мы используем тестирование на основе равенства epsilon или ULP.
Однако, интересно, есть ли случаи, когда использование ==
отлично?
Посмотрите на этот простой фрагмент, какие случаи гарантированно преуспеют?
void fn(float a, float b) {
float l1 = a/b;
float l2 = a/b;
if (l1==l1) { } // case a)
if (l1==l2) { } // case b)
if (l1==a/b) { } // case c)
if (l1==5.0f/3.0f) { } // case d)
}
int main() {
fn(5.0f, 3.0f);
}
Примечание. Я проверил это и это, но они не охватывают (все) мои дела.
Примечание2: Кажется, что я должен добавить дополнительную информацию, поэтому ответы могут быть полезны на практике: я хотел бы знать:
- что говорит стандарт C++
- что произойдет, если реализация C++ следует за IEEE-754
Это единственное релевантное выражение, которое я нашел в текущем проекте стандарта:
Представление значений типов с плавающей точкой - это реализация -d. [Примечание: в этом документе нет требований к точности операций с плавающей запятой; см. также [support.limits]. - конечная нота]
Таким образом, означает ли это, что даже "случай a" - это реализация? Я имею в виду, что l1==l1
определенно является операцией с плавающей запятой. Итак, если реализация "неточна", то может ли l1==l1
быть ложным?
Я думаю, что этот вопрос не является дубликатом. С плавающей точкой == когда-либо ОК? , Этот вопрос не касается ни одного из случаев, о которых я прошу. Тот же предмет, другой вопрос. Я хотел бы получить ответы конкретно на случай a) -d), для которого я не могу найти ответы в дублированном вопросе.
Ответы
Ответ 1
Однако, интересно, есть ли случаи, когда использование == отлично?
Конечно, есть. Одна категория примеров - это обычаи, которые не включают вычислений, например, сеттеры, которые должны выполняться только при изменениях:
void setRange(float min, float max)
{
if(min == m_fMin && max == m_fMax)
return;
m_fMin = min;
m_fMax = max;
// Do something with min and/or max
emit rangeChanged(min, max);
}
См. Также С плавающей точкой == когда-либо ОК? и является ли плавающей точкой == когда-либо ОК? ,
Ответ 2
Проституческие дела могут "работать". Практические случаи могут по-прежнему терпеть неудачу. Еще одна проблема заключается в том, что часто оптимизация приводит к небольшим изменениям в способе расчета, чтобы символически результаты были равными, но численно они различны. Приведенный выше пример может, теоретически, потерпеть неудачу в таком случае. Некоторые компиляторы предлагают возможность получения более последовательных результатов при стоимости исполнения. Я бы посоветовал "всегда" избегать равенства чисел с плавающей запятой.
Равенство физических измерений, а также хранящихся в цифровом виде поплавков часто бессмысленно. Поэтому, если вы сравниваете, если float равны в вашем коде, вы, вероятно, делаете что-то неправильно. Обычно вам требуется больше или меньше того или в пределах допуска. Часто код можно переписать, чтобы избежать подобных проблем.
Ответ 3
Только a) и b) гарантированно преуспеют в любой разумной реализации (см. Ниже подробное описание ниже), поскольку они сравнивают два значения, которые были получены таким же образом и округлены до точности float
. Следовательно, обе сравниваемые значения гарантированно будут идентичны последнему бит.
Случай c) и d) может потерпеть неудачу, поскольку вычисление и последующее сравнение могут выполняться с большей точностью, чем float
. Различного округления double
должно быть достаточно, чтобы не пройти тест.
Обратите внимание, что случаи a) и b) могут по-прежнему терпеть неудачу, если задействованы бесконечности или NAN.
ЮРИДИЧЕСКАЯ
Используя рабочий проект стандарта N3242 С++ 11, я нахожу следующее:
В тексте, описывающем выражение присваивания, явно указано, что преобразование типа имеет место, [expr.ass] 3:
Если левый операнд не относится к типу класса, выражение неявно преобразуется (раздел 4) в cv-неквалифицированный тип левого операнда.
Пункт 4 относится к стандартным преобразованиям [conv], которые содержат следующие преобразования конверсий с плавающей запятой, [conv.double] 1:
Значение класса с плавающей запятой может быть преобразовано в prvalue другого типа с плавающей точкой. Если исходное значение может быть точно представлено в типе назначения, результатом преобразования является точное представление. Если исходное значение находится между двумя соседними целевыми значениями, результатом преобразования является определенный реализацией выбор любого из этих значений. В противном случае поведение не определено.
(Акцент мой.)
Таким образом, мы гарантируем, что результат преобразования фактически определен, если мы не имеем дело со значениями вне представленного диапазона (например, float a = 1e300
, который является UB).
Когда люди думают, что "внутреннее представление с плавающей запятой может быть более точным, чем видимое в коде", они думают о следующем предложении в стандарте, [expr] 11:
Значения плавающих операндов и результаты плавающих выражений могут быть представлены в большей точности и дальности, чем требуемые типом; типы не изменяются.
Обратите внимание, что это относится к операндам и результатам, а не к переменным. Это подчеркивается прилагаемой сноской 60:
Операторы литья и назначения должны по-прежнему выполнять свои конкретные преобразования, как описано в 5.4, 5.2.9 и 5.17.
(Я думаю, это примечание, которое Maciej Piechotka означало в комментариях - нумерация, похоже, изменилась в версии стандарта, который он использовал).
Итак, когда я говорю float a = some_double_expression;
, У меня есть гарантия, что результат выражения фактически закругленную быть представим float
(ссылающегося UB только если значение выходит за пределы площадки), и будет относиться к этому округлому значению впоследствии. a
Реализация действительно может указать, что результат округления является случайным и, таким образом, нарушает случаи а) и б). Однако реализация Sane не сделает этого.
Ответ 4
Предполагая, что в семантике IEEE 754 есть определенные случаи, когда вы можете это сделать. Обычные вычисления числа с плавающей запятой являются точными, когда они могут быть, например, включают (но не ограничиваются) все основные операции, в которых операнды и результаты являются целыми числами.
Поэтому, если вы знаете, что вы не делаете ничего, что может привести к чему-то непредсказуемому, вы в порядке. Например
float a = 1.0f;
float b = 1.0f;
float c = 2.0f;
assert(a + b == c); // you can safely expect this to succeed
Ситуация действительно становится плохой, если у вас есть вычисления с результатами, которые не являются точно представляемыми (или которые связаны с неточными операциями), и вы изменяете порядок операций.
Обратите внимание, что стандарт C++ сам по себе не гарантирует семантику IEEE 754, но это то, с чем вы можете столкнуться большую часть времени.
Ответ 5
Случай (a) терпит неудачу, если a == b == 0.0
. В этом случае операция дает NaN и по определению (IEEE, а не C) NaN ≠ NaN.
Случаи (b) и (c) могут терпеть неудачу при параллельном вычислении, когда циклические режимы с плавающей запятой (или другие режимы вычислений) изменяются в середине выполнения этого потока. Видно это на практике, к сожалению.
Случай (d) может быть другим, потому что компилятор (на какой-то машине) может выбрать постоянное вычисление 5.0f/3.0f
и заменить его на постоянный результат (неопределенной точности), тогда как a/b
должен быть вычислен на время выполнения на целевой машине (что может быть радикально отличается). Фактически промежуточные вычисления могут выполняться произвольной точностью. Я видел различия на старых архитектурах Intel, когда промежуточное вычисление выполнялось в 80-битной плавающей запятой, формат, который язык даже не поддерживал напрямую.
Ответ 6
По моему скромному мнению, вы не должны полагаться на оператор ==
потому что у него много угловых случаев. Самая большая проблема - округление и расширенная точность. В случае x86 операции с плавающей запятой могут выполняться с большей точностью, чем вы можете хранить в переменных (если вы используете сопроцессоры, операции SSE IIRC используют ту же точность, что и хранилище).
Обычно это хорошо, но это вызывает такие проблемы, как: 1./2 != 1./2
потому что одно значение является переменной формы, а второе - регистром с плавающей запятой. В простейших случаях он будет работать, но если вы добавите другие операции с плавающей запятой, компилятор может решить разбить некоторые переменные на стек, изменив их значения, тем самым изменив результат сравнения.
Чтобы иметь 100% уверенность, вам нужно посмотреть на сборку и посмотреть, какие операции были выполнены ранее по обоим значениям. Даже порядок может изменить результат в нетривиальных случаях.
В целом, что такое использование ==
? Вы должны использовать стабильные алгоритмы. Это означает, что они работают, даже если значения не равны, но они все равно дают одинаковые результаты. Единственное место, где я знаю, где ==
может быть полезным, это сериализация/десериализация, где вы знаете, какой результат вы хотите в точности, и вы можете изменить сериализацию для архивирования своей цели.