Равенство с плавающей точкой - PullRequest
0 голосов
/ 02 июля 2018

Общеизвестно, что нужно быть осторожным при сравнении значений с плавающей запятой. Обычно вместо == мы используем некоторые тесты на равенство на основе 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

Это единственное соответствующее заявление, которое я нашел в текущем проекте стандарта :

Представление значений для типов с плавающей запятой определяется реализацией. [Примечание: этот документ не предъявляет никаких требований к точности операций с плавающей запятой; см. также [support.limits]. - конец примечания]

Значит ли это, что даже "case a)" определяется реализацией? Я имею в виду, l1==l1 определенно является операцией с плавающей точкой. Итак, если реализация «неточна», то может ли l1==l1 быть ложным?


Я думаю, что этот вопрос не является дубликатом С плавающей точкой == когда-либо в порядке? . Этот вопрос не касается ни одного из случаев, которые я задаю. Та же тема, другой вопрос. Я хотел бы получить ответы специально для случая а) -г), для которого я не могу найти ответы в дублированном вопросе.

Ответы [ 6 ]

0 голосов
/ 04 июля 2018

Случай (а) завершается неудачей, если a == b == 0.0. В этом случае операция дает NaN, и по определению (IEEE, а не C) NaN ≠ NaN.

Случаи (b) и (c) могут давать сбой при параллельном вычислении, когда режимы с плавающей запятой (или другие режимы вычислений) изменяются в середине выполнения этого потока. К сожалению, видел это на практике.

Случай (d) может отличаться, потому что компилятор (на некотором компьютере) может выбрать постоянное сгибание вычисления 5.0f/3.0f и заменить его постоянным результатом (с неопределенной точностью), тогда как a/b должно быть вычислено во время выполнения на целевой машине (которая может быть радикально другой). Фактически промежуточные вычисления могут выполняться с произвольной точностью. Я видел различия в старых архитектурах Intel, когда промежуточные вычисления выполнялись в 80-битном формате с плавающей запятой, формате, который язык даже не поддерживал напрямую.

0 голосов
/ 02 июля 2018

Предполагая семантику 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, но с этим можно ожидать большую часть времени.

0 голосов
/ 02 июля 2018

По моему скромному мнению, вы не должны полагаться на оператор ==, потому что он имеет много угловых случаев. Самая большая проблема - округление и повышенная точность. В случае x86, операции с плавающей точкой могут выполняться с большей точностью, чем вы можете хранить в переменных (если вы используете сопроцессоры, операции IIRC SSE используют ту же точность, что и хранилище).

Обычно это хорошо, но это вызывает такие проблемы, как: 1./2 != 1./2 потому что одно значение является переменной формы, а второе - из регистра с плавающей запятой. В простейших случаях это будет работать, но если вы добавите другие операции с плавающей запятой, компилятор может решить разделить некоторые переменные в стеке, изменив их значения, тем самым изменив результат сравнения.

Чтобы иметь 100% уверенность, вам нужно посмотреть на сборку и посмотреть, какие операции были выполнены ранее для обоих значений. Даже порядок может изменить результат в нетривиальных случаях.

В целом, какой смысл использовать ==? Вы должны использовать алгоритмы, которые являются стабильными. Это означает, что они работают, даже если значения не равны, но они все равно дают те же результаты. Единственное место, которое я знаю, где == может быть полезным, - это сериализация / десериализация, где вы точно знаете, какой результат вы хотите получить, и вы можете изменить сериализацию для архивирования своей цели.

0 голосов
/ 02 июля 2018

Придуманные дела могут «работать». Практические случаи все еще могут потерпеть неудачу. Еще одна проблема заключается в том, что зачастую оптимизация приводит к небольшим изменениям в способе расчета, поэтому символически результаты должны быть одинаковыми, но численно они различаются. Приведенный выше пример теоретически может потерпеть неудачу в таком случае. Некоторые компиляторы предлагают возможность получения более согласованных результатов при снижении производительности. Я бы посоветовал «всегда» избегать равенства чисел с плавающей точкой.

Равенство физических измерений, а также чисел с плавающей запятой, часто не имеет смысла. Так что, если вы сравниваете поплавки в вашем коде, вы, вероятно, делаете что-то не так. Вы обычно хотите больше или меньше или в пределах допуска. Часто код можно переписать, чтобы избежать подобных проблем.

0 голосов
/ 02 июля 2018

Только a) и b) гарантированно преуспеют в любой вменяемой реализации (подробности см. Ниже), так как они сравнивают два значения, которые были получены одинаковым образом и округлены до точности float. Следовательно, оба сравниваемых значения гарантированно будут идентичны последнему биту.

Случаи c) и d) могут потерпеть неудачу, потому что вычисление и последующее сравнение могут быть выполнены с большей точностью, чем float. Различного округления double должно быть достаточно, чтобы не пройти тест.

Обратите внимание, что случаи a) и b) могут все же потерпеть неудачу, если задействованы бесконечности или NAN.


ЮРИДИЧЕСКАЯ

Используя рабочий проект стандарта N3242 C ++ 11, я обнаружил следующее:

В тексте, описывающем выражение присваивания, прямо указано, что происходит преобразование типов, [expr.ass] 3:

Если левый операнд не относится к типу класса, выражение неявно преобразуется (пункт 4) в cv-неквалифицированный тип левого операнда.

Раздел 4 относится к стандартным преобразованиям [conv], которые содержат следующие преобразования в числах с плавающей запятой, [conv.double] 1:

Значение типа с плавающей запятой может быть преобразовано в значение другого типа с плавающей запятой. Если исходное значение может быть точно представлено в целевом типе, результат преобразования представление. Если исходное значение находится между двумя соседними целевыми значениями, результат преобразования является определенным реализацией выбором любого из этих значений. В противном случае поведение не определено.

(Акцент мой.)

Таким образом, у нас есть гарантия того, что результат преобразования фактически определен, если только мы не имеем дело со значениями за пределами представимого диапазона (например, float a = 1e300, то есть UB).

Когда люди думают, что «внутреннее представление с плавающей точкой может быть более точным, чем видимое в коде», они думают о следующем предложении в стандарте, [expr] 11:

Значения плавающих операндов и результаты плавающих выражений могут быть представлены в большем точность и дальность, чем требуется типом; типы не меняются.

Обратите внимание, что это относится к операндам и результатам , а не к переменным. Это подчеркивается прилагаемой сноской 60:

Операторы приведения и присваивания по-прежнему должны выполнять свои конкретные преобразования, как описано в 5.4, 5.2.9 и 5.17.

(Я думаю, это сноска, которую Мачей Пехотка имел в виду в комментариях - нумерация, похоже, изменилась в используемой им версии стандарта.)

Итак, когда я говорю float a = some_double_expression;, у меня есть гарантия, что результат выражения на самом деле округляется до значения, представляемого float (вызывая UB, только если значение выходит за пределы), и a будет ссылаться на это округленное значение впоследствии.

Реализация может действительно указывать, что результат округления является случайным, и, таким образом, разбивать случаи a) и b). Однако здравомыслящие реализации этого не сделают.

0 голосов
/ 02 июля 2018

Однако, мне интересно, есть ли случаи, когда использование == совершенно нормально?

Конечно, есть. Одной категорией примеров являются способы использования, которые не требуют вычислений, например сеттеры, которые должны выполняться только при изменениях:

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);
}

См. Также Плавающая точка == когда-либо в порядке? и Плавающая точка == всегда в порядке? .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...