Является ли плавающая точка == когда-либо ОК?

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

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

Ответ 1

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

  • Существуют ли случаи, когда 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 никогда не будет * ОК, потому что он почти не поддается.

* При малых значениях никогда.

Ответ 2

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

Ответ 3

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

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

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

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

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

Примечание:

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

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

Ответ 4

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

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

/* let 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));
}

Я бы предпочел не объяснять метод bisection, но при этом подчеркнуть условие остановки. Он имеет точно обсуждаемую форму: (a == a+d), где обе стороны являются поплавками: a - наше текущее приближение корня уравнения, а d - наша текущая точность. Учитывая предварительное условие алгоритма - что должен быть корнем между range_start и range_end - мы гарантируем на каждой итерации, что корень остается между a и a+d, а d сокращается наполовину на каждом шаге, уменьшая границы.

И затем, после нескольких итераций, d становится настолько малым, что во время добавления с a он округляется до нуля! То есть a+d оказывается ближе к a, затем к любому другому float; и поэтому FPU округляет его до ближайшего значения: к самому a. Это можно легко проиллюстрировать вычислением на гипотетической вычислительной машине; пусть у него есть 4-значная десятичная мантисса и некоторый большой диапазон экспонентов. Тогда какой результат машина должна дать 2.131e+02 + 7.000e-3? Точный ответ 213.107, но наша машина не может представлять такое число; он должен округлить его. И 213.107 намного ближе к 213.1, чем к 213.2 - поэтому округленный результат становится 2.131e+02 - небольшое слагаемое исчезало, округленное до нуля. Точно так же гарантируется, что происходит на некоторой итерации нашего алгоритма - и в этот момент мы больше не можем продолжать. Мы нашли корень максимально возможной точности.

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


Update

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

Возможно, вы уже знаете, что в исчислении нет понятия "маленькое число": для любого действительного числа вы можете легко найти бесконечно много даже более мелких. Проблема в том, что одним из тех "еще меньших" может быть то, что мы действительно ищем; это может быть корень нашего уравнения. Хуже того, для разных уравнений могут быть разные корни (например, 2.51e-8 и 1.38e-8), оба из которых будут приближаться к тому же числу, если наше условие остановки будет выглядеть как d < 1e-6. Какое бы "небольшое число" вы выберете, многие корни, которые были бы правильно найдены с максимальной точностью с условием остановки a == a+d, будут испорчены, если "эпсилон" будет слишком большим.

Правда, однако, что в числах с плавающей запятой экспонента имеет ограниченный диапазон, поэтому вы можете найти наименьший ненулевой положительный номер FP (например, 1e-45 denorm для IEEE 754 с одной точностью FP). Но это бесполезно! 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, алгоритм не будет застревать в бесконечном цикле с огромными корнями (он очень хорошо мог бы с (b-a) < eps), но все равно будет выполнять ненужные итерации при сжатии d ниже точность a.

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

Ответ 5

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

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

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

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

Ответ 6

Единственным случаем, когда я когда-либо использовал == (или !=) для float, является следующее:

if (x != x)
{
    // Here x is guaranteed to be Not a Number
}

и я должен признать, что я виновен в использовании Not A Number в качестве волшебной константы с плавающей запятой (используя numeric_limits<double>::quiet_NaN() в С++).

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

Ответ 7

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

Ответ 8

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

Ответ 9

Да. 1/x будет действительным, если x==0. Здесь вам не нужен неточный тест. 1/0.00000001 отлично. Я не могу придумать ни одного другого случая - вы даже не можете проверить tan(x) на x==PI/2

Ответ 10

Скажем, у вас есть функция, которая масштабирует массив поплавков по постоянному коэффициенту:

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 широко использует такие методы.

Ответ 11

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

Пример:

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

  return cachedValue;
}

Ответ 12

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

Предположим, например, что у вас есть программа, которая выводит на экран значения с плавающей запятой и что если значение с плавающей запятой будет в точности равным M_PI, тогда вы хотите, чтобы он распечатывал "pi" вместо, Если значение отклоняется от маленького бита от точного двойного представления M_PI, оно будет печатать вместо него двойное значение, которое является одинаково допустимым, но немного менее читаемым для пользователя.

Ответ 13

На мой взгляд, сравнение в отношении равенства (или некоторой эквивалентности) является требованием в большинстве ситуаций: стандартные контейнеры С++ или алгоритмы с подразумеваемым функтором сравнения равенства, например std:: unordered_set, Требует, чтобы этот компаратор был отношением эквивалентности См. UnorderedAssociativeContainer. К сожалению, сравнение с epsilon как в abs(a - b) < epsilon не дает отношения эквивалентности, поскольку оно теряет транзитивность. Это, скорее всего, поведение undefined, в частности два "почти равных" числа с плавающей запятой могут давать разные хэши; это может привести к тому, что unordered_set окажется недопустимым. Лично я большую часть времени использовал бы для чисел с плавающей запятой, если бы любые операнды не принимали никаких вычислений fpu. С контейнерами и контейнерными алгоритмами, где задействуются только чтение/запись, == (или любое отношение эквивалентности) является самым безопасным.

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

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

Ответ 14

У меня есть программа рисования, которая принципиально использует плавающую точку для своей системы координат, так как пользователю разрешено работать с любой степенью детализации/масштабирования. То, что они рисуют, содержит строки, которые могут быть согнуты в созданных ими точках. Когда они перетаскивают одну точку поверх другой, они объединяются.

Чтобы сделать "правильное" сравнение с плавающей запятой, мне пришлось бы придумать некоторый диапазон, в котором можно было бы считать точки одинаковыми. Поскольку пользователь может увеличивать масштаб до бесконечности и работать в этом диапазоне, и поскольку я не мог заставить кого-либо совершить какой-либо диапазон, мы просто используем '==', чтобы увидеть, совпадают ли точки. Иногда возникает проблема, когда точки, которые должны быть точно такими же, отключены на .000000000001 или что-то (особенно около 0,0), но обычно это работает нормально. Это должно быть сложно объединить точки без включенной привязки... или, по крайней мере, того, как работала оригинальная версия.

Он периодически отбрасывает группу тестирования, но их проблема: p

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