Плавающая точка == когда-либо в порядке? - PullRequest
52 голосов
/ 13 января 2011

Только сегодня я наткнулся на стороннее программное обеспечение, которое мы используем, и в его примере кода было что-то вроде этого:

// 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?

Ответы [ 14 ]

37 голосов
/ 13 января 2011

Да, вам гарантировано, что целые числа, включая 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 !!
34 голосов
/ 13 января 2011

Есть два способа ответить на этот вопрос:

  1. Есть ли случаи, когда float == float дает правильный результат?
  2. Есть ли случаи, когда float == float является приемлемым кодированием?

Ответ на (1): Да, иногда.Но это будет хрупким, что приводит к ответу на (2): Нет. Не делай этого.Вы просите о странных ошибках в будущем.

Что касается вызова формы foo(BAR): в этом конкретном случае сравнение вернет true, но когда вы пишете foo, вы незнать (и не должно зависеть) как это называется.Например, вызов foo(BAR) будет в порядке, но foo(BAR * 2.0 / 2.0) (или даже, может быть, foo(BAR * 1.0) в зависимости от того, насколько компилятор оптимизирует ситуацию) прервется.Вы не должны полагаться на то, что вызывающий абонент не выполняет никакой арифметики!

Короче говоря, даже если a == b будет работать в некоторых случаях, вам действительно не следует полагаться на это.Даже если вы можете гарантировать семантику вызовов сегодня, возможно, вы не сможете гарантировать их на следующей неделе, так что избавьте себя от боли и не используйте ==.

На мой взгляд, float == float никогда не бывает* ОК, потому что это практически невозможно.

* Для небольших значений никогда.

14 голосов
/ 10 февраля 2011

Другие ответы довольно хорошо объясняют, почему использование == для чисел с плавающей запятой опасно. Я только что нашел один пример, который достаточно хорошо иллюстрирует эти опасности.

На платформе x86 вы можете получить странные результаты с плавающей запятой для некоторых вычислений, которые не из-за проблем округления, присущих выполняемым вами вычислениям. Эта простая программа на C иногда выдает «error»:

#include <stdio.h>

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 программы обычно используют x87 FPU для вычислений с плавающей запятой. Внутренние вычисления x87 выполняются с большей точностью, чем обычные double, поэтому значения double необходимо округлять при их хранении в памяти. Это означает, что круговая передача x87 -> RAM -> x87 теряет точность, и, следовательно, результаты вычислений различаются в зависимости от того, были ли промежуточные результаты переданы через RAM или все они остались в регистрах FPU. Это, конечно, решение компилятора, поэтому ошибка проявляется только для определенных компиляторов и настроек оптимизации: - (.

Подробнее см. Ошибка GCC: http://gcc.gnu.org/bugzilla/show_bug.cgi?id=323

Скорее страшно ...

Дополнительные примечания:

Ошибки такого рода, как правило, довольно сложно отлаживать, потому что разные значения становятся одинаковыми после попадания в ОЗУ.

Таким образом, если, например, вы расширите вышеупомянутую программу, чтобы фактически распечатать битовые комбинации y и y2 сразу после их сравнения, вы получите точно такое же значение . Чтобы напечатать значение, оно должно быть загружено в ОЗУ для передачи в какую-либо функцию печати, например printf, и это приведет к исчезновению различий ...

8 голосов
/ 13 января 2011

Идеально подходит для целочисленных значений даже в форматах с плавающей запятой

Но краткий ответ: "Нет, не используйте ==."

Как ни странноФормат с плавающей запятой работает «идеально», то есть с точной точностью, при работе с целочисленными значениями в пределах диапазона формата.Это означает, что если вы придерживаетесь удвоенных значений, вы получите совершенно хорошие целые числа с чуть более 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 ​​цифрами.

Таким образом, ответ таков: , хотя точное равенство теоретически может иметь смысл в узких случаях, его лучше избегать.

7 голосов
/ 10 февраля 2011

Я попытаюсь привести более-менее реальный пример законного, значимого и полезного тестирования на равенство с плавающей точкой.

#include <stdio.h>
#include <math.h>

/* 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 онокругляется до нуля!То есть a+d оказывается ближе к a, а затем к любому другому значению с плавающей запятой ;и поэтому FPU округляет его до ближайшего значения: до a.Это может быть легко проиллюстрировано расчетом на гипотетической вычислительной машине;пусть у него будет 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 denorm для FP с одинарной точностью IEEE 754),Но это бесполезно!while (d < 1e-45) {...} будет зацикливаться вечно, предполагая одинарную точность (положительное ненулевое значение) d.

SettiПомимо этих патологических краевых случаев, любой выбор "малого числа" в условии остановки 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, алгоритм не будет застревать в бесконечном цикле с огромными корнями (это очень хорошо может быть с (b-a) < eps), но все равно будет выполнять ненужные итерации во время сжатия d ниже точность a.

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

7 голосов
/ 13 января 2011

Единственный случай, когда я использую == (или !=) для чисел с плавающей запятой:Константа с плавающей запятой (используется numeric_limits<double>::quiet_NaN() в C ++).

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

4 голосов
/ 13 января 2011

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

2 голосов
/ 30 июня 2017

По моему мнению, сравнение на равенство (или некоторую эквивалентность) является требованием в большинстве ситуаций: стандартные контейнеры или алгоритмы C ++ с подразумеваемым функтором сравнения на равенство, например, например, std :: unordered_set, требуют, чтобы этот компаратор был отношением эквивалентности (см. Именованные требования C ++: UnorderedAssociativeContainer ).

К сожалению, сравнение с эпсилоном, как в abs(a - b) < epsilon, не дает отношения эквивалентности, поскольку теряет транзитивность. Скорее всего, это неопределенное поведение, в частности, два «почти равных» числа с плавающей точкой могут давать разные хэши; это может перевести unordered_set в недопустимое состояние. Лично я большую часть времени буду использовать == для чисел с плавающей запятой, , если для любых операндов не будет задействован какой-либо тип вычисления FPU. С контейнерами и контейнерными алгоритмами, где задействованы только чтение / запись, == (или любое отношение эквивалентности) является самым безопасным.

abs(a - b) < epsilon - более или менее критерий сходимости, аналогичный пределу. Я считаю это соотношение полезным, если мне нужно убедиться, что между двумя вычислениями выполняется математическая идентичность (например, PV = nRT или расстояние = время * скорость).

Короче говоря, используйте == тогда и только тогда, когда вычисление с плавающей запятой не происходит; никогда не используйте abs(a-b) < e в качестве предиката равенства;

2 голосов
/ 17 января 2011

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

1 голос
/ 17 июля 2011

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

Пример:

float someFunction (float argument)
{
  // I really want bit-exact comparison here!
  if (argument != lastargument)
  {
    lastargument = argument;
    cachedValue = very_expensive_calculation (argument);
  }

  return cachedValue;
}
...