Насколько опасно сравнивать значения с плавающей запятой?


391

Я знаю, UIKitиспользует CGFloatиз-за разрешения независимой системы координат.

Но каждый раз, когда я хочу проверить, например, frame.origin.xвызывает ли 0это у меня тошноту:

if (theView.frame.origin.x == 0) {
    // do important operation
}

Не CGFloatуязвима к ложным срабатываниям при сравнении с ==, <=, >=, <, >? Это с плавающей точкой, и у них есть проблемы неточности: 0.0000000000041например.

Objective-CОбрабатывает ли это внутренне при сравнении или может случиться так, что a, origin.xкоторый читается как ноль, не сравнивается с 0истинным?

Ответы:


466

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

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

Что каждый компьютерщик должен знать об арифметике с плавающей точкой

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

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

  1. Тот факт, что многие значения, которые вы можете записать в источнике или прочитать с помощью scanfили strtod, не существуют как значения с плавающей запятой и автоматически преобразуются в ближайшее приближение. Об этом говорил ответ demon9733.

  2. Тот факт, что многие результаты округляются из-за отсутствия достаточной точности для представления фактического результата. Простой пример, где вы можете увидеть это добавление x = 0x1fffffeи y = 1плавание. Здесь он xимеет 24 бита точности в мантиссе (хорошо) и yимеет всего 1 бит, но когда вы добавляете их, их биты не находятся в перекрывающихся местах, и результат должен будет иметь 25 бит точности. Вместо этого он округляется ( 0x2000000в режиме округления по умолчанию).

  3. Тот факт, что многие результаты округляются из-за необходимости бесконечного количества мест для правильного значения. Это включает в себя как рациональные результаты, такие как 1/3 (с которым вы знакомы из десятичной дроби, где она занимает бесконечно много мест), так и 1/10 (которая также занимает бесконечно много мест в двоичной системе, поскольку 5 не является степенью 2), а также иррациональные результаты, такие как квадратный корень всего, что не является идеальным квадратом.

  4. Двойное округление. В некоторых системах (в частности, в x86) выражения с плавающей запятой оцениваются с большей точностью, чем их номинальные типы. Это означает, что когда происходит один из указанных выше типов округления, вы получите два шага округления: сначала округление результата до типа с более высокой точностью, затем округление до конечного типа. В качестве примера рассмотрим, что происходит в десятичном виде, если округлить 1.49 до целого числа (1), а не в том, что происходит, если сначала округлить его до одного десятичного знака (1.5), а затем округлить полученный результат до целого числа (2). На самом деле это одна из самых неприятных областей в плавающей точке, поскольку поведение компилятора (особенно для глючных, не соответствующих стандарту компиляторов, таких как GCC) непредсказуемо.

  5. Трансцендентные функции ( trig, exp, logи т.д.) не определены , чтобы правильно округленные результаты; результат только что указан, чтобы быть правильным в пределах одной единицы в последнем месте точности (обычно упоминаемый как 1ulp ).

Когда вы пишете код с плавающей запятой, вам нужно помнить о том, что вы делаете с числами, которые могут привести к неточности результатов, и делать соответствующие сравнения. Часто имеет смысл сравнивать с «эпсилоном», но этот эпсилон должен основываться на величине сравниваемых чисел , а не на абсолютной константе. (В случаях, когда сработает абсолютная постоянная эпсилон, это сильно указывает на то, что фиксированная точка, а не с плавающей точкой, является правильным инструментом для работы!)

Изменить: В частности, проверка эпсилон-относительной величины должна выглядеть примерно так:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

Где FLT_EPSILONнаходится константа float.h(замените ее DBL_EPSILONна doubles или LDBL_EPSILONдля long doubles), и Kвы выбираете такую ​​константу, чтобы накопленная ошибка ваших вычислений была определенно ограничена Kединицами в последнем месте (и если вы не уверены, что получили ошибку связанный расчет правильно, сделайте Kв несколько раз больше, чем ваши вычисления говорят, что это должно быть).

Наконец, обратите внимание, что если вы используете это, может потребоваться некоторая особая осторожность вблизи нуля, так FLT_EPSILONкак не имеет смысла для ненормированных. Быстрое решение было бы сделать это:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

и аналогично заменить, DBL_MINесли использовать удваивается.


25
fabs(x+y)проблематично, если xи y(может) иметь другой знак. Тем не менее, хороший ответ против потока культовых сравнений.
Даниэль Фишер

27
Если xи yесть разные признаки, это не проблема. Правая будет «слишком мал», но так xи yимеют разный знак, они не должны сравнивать равные в любом случае. (Если они не настолько малы, чтобы быть denormal, но тогда второй ловит случае это)
R .. GitHub СТОП ПОМОГАЯ ICE

4
Мне любопытно ваше утверждение: «особенно для глючных, несовместимых компиляторов, таких как GCC». Действительно ли GCC глючит и не соответствует?
Николас Озимица

3
Поскольку вопрос помечен iOS, стоит отметить, что компиляторы Apple (как clang, так и сборки gcc от Apple) всегда использовали FLT_EVAL_METHOD = 0 и стараются быть абсолютно строгими в отношении отсутствия избыточной точности. Если вы обнаружите какие-либо нарушения этого, пожалуйста, отправляйте сообщения об ошибках.
Стивен Кэнон

17
«Во-первых, значения с плавающей точкой не являются« случайными »в своем поведении. Точное сравнение может иметь смысл и имеет смысл при большом количестве реальных применений». - Всего два предложения и уже заработал +1! Это одно из самых тревожных заблуждений людей при работе с плавающей точкой.
Кристиан Рау

36

Поскольку 0 точно представляется в виде числа с плавающей точкой IEEE754 (или с использованием любой другой реализации чисел fp, с которой я когда-либо работал), сравнение с 0, вероятно, безопасно. Однако вы можете быть укушены, если ваша программа вычисляет значение (такое как theView.frame.origin.x), которое, по вашему мнению, должно быть равно 0, но которое ваши вычисления не могут гарантировать равным 0.

Чтобы уточнить немного, вычисление, такое как:

areal = 0.0

(если ваш язык или система не сломаны) создаст значение, такое что (areal == 0.0) вернет true, но другое вычисление, такое как

areal = 1.386 - 2.1*(0.66)

может нет.

Если вы можете быть уверены, что ваши вычисления дают значения, равные 0 (а не только то, что они дают значения, которые должны быть равны 0), тогда вы можете пойти дальше и сравнить значения fp с 0. Если вы не можете убедиться в необходимой степени Лучше всего придерживаться обычного подхода «терпимого равенства».

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

Для Angry Birds не так уж и опасно.


11
На самом деле, 1.30 - 2*(0.65)это прекрасный пример выражения, которое, очевидно, оценивается в 0.0, если ваш компилятор реализует IEEE 754, потому что числа, представленные как 0.65и 1.30имеющие одинаковые значения, и умножение на два, очевидно, является точным.
Паскаль Куок

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

22

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

С плавающей точкой в ​​графике все в порядке! Но нет необходимости сравнивать поплавки напрямую. Зачем вам это нужно? Графика использует поплавки для определения интервалов. И сравнение, если поплавок находится в интервале, также определяемом поплавками, всегда хорошо определено и просто должно быть последовательным, не точным или точным! Пока пиксель (который также является интервалом!) Может быть назначен, это все графические потребности.

Так что, если вы хотите проверить, находится ли ваша точка вне диапазона [0..width [, это нормально. Просто убедитесь, что вы определяете включение последовательно. Например, всегда определяйте внутри is (x> = 0 && x <width). То же самое касается тестов на пересечение или попадание.

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


13

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

Говоря о идеально представимых значениях, вы получаете 24-битный диапазон в представлении степени двух (одинарная точность). Таким образом, 1, 2, 4 отлично представимы, как .5, .25 и .125. Пока все ваши важные биты в 24-битах, вы золотой. Таким образом, 10.625 могут быть представлены точно.

Это здорово, но под давлением быстро развалится. На ум приходят два сценария: 1) Когда происходит расчет. Не верьте этому sqrt (3) * sqrt (3) == 3. Такого не будет. И это, вероятно, не будет в эпсилоне, как предполагают некоторые другие ответы. 2) Когда задействован любой не-сила-2 (NPOT). Так что это может звучать странно, но 0.1 - это бесконечный ряд в двоичном коде, и поэтому любые вычисления с таким числом будут неточными с самого начала.

(Да, и в первоначальном вопросе упоминалось сравнение с нулем. Не забывайте, что -0.0 также является совершенно допустимым значением с плавающей запятой.)


11

«Правильный ответ» закрывает выбор K. Выбор Kзаканчивается так же, как выбор, VISIBLE_SHIFTно выбор Kменее очевиден, потому что, в отличие от VISIBLE_SHIFTнего, он не основан ни на каком свойстве отображения. Таким образом, выберите свой яд - выберите Kили выберите VISIBLE_SHIFT. Этот ответ призывает к выбору, VISIBLE_SHIFTа затем демонстрирует трудности выбора K]

Именно из-за ошибок округления не следует использовать сравнение «точных» значений для логических операций. В вашем конкретном случае позиции на визуальном дисплее не может иметь значения, будет ли позиция 0,0 или 0,0000000003 - разница невидима для глаза. Так что ваша логика должна быть примерно такой:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

Однако, в конце концов, «невидимый для глаз» будет зависеть от ваших свойств дисплея. Если вы можете верхнюю границу дисплея (вы должны быть в состоянии); затем выберите VISIBLE_SHIFTчасть этой верхней границы.

Теперь «правильный ответ» опирается на Kтак что давайте рассмотрим выбор K. «Правильный ответ» выше говорит:

K - это постоянная, которую вы выбираете так, чтобы накопленная ошибка ваших вычислений была определенно ограничена K единицами в последнем месте (и если вы не уверены, что вы правильно рассчитали границы ошибок, сделайте K в несколько раз больше, чем ваши вычисления скажи что должно быть)

Так что нам нужно K. Если получить Kболее сложно, менее интуитивно, чем выбрать мой, VISIBLE_SHIFTтогда вы решите, что работает для вас. Чтобы найти, Kмы собираемся написать тестовую программу, которая смотрит на кучу Kзначений, чтобы мы могли видеть, как она себя ведет. Должно быть очевидно, как выбрать K, если «правильный ответ» пригоден для использования. Нет?

В качестве «правильного ответа» мы будем использовать детали:

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Давайте просто попробуем все значения K:

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

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

Ах, так что K должно быть 1e16 или больше, если я хочу, чтобы 1e-13 было «ноль».

Итак, я бы сказал, у вас есть два варианта:

  1. Проведите простое вычисление эпсилона, используя свое инженерное суждение для значения «эпсилон», как я и предлагал. Если вы работаете с графикой, а «ноль» означает «видимое изменение», тогда изучите ваши визуальные ресурсы (изображения и т. Д.) И определите, каким может быть эпсилон.
  2. Не пытайтесь выполнять какие-либо вычисления с плавающей запятой, пока не прочитаете ссылку на не-культовый ответ (и не получите свою степень доктора наук в процессе), а затем воспользуетесь своим неинтуитивным суждением для выбора K.

10
Одним из аспектов независимости от разрешения является то, что вы не можете точно сказать, что такое «видимый сдвиг» во время компиляции. То, что невидимо на экране Super-HD, вполне может быть очевидно на экране с крошечной попкой. Надо хотя бы сделать это функцией размера экрана. Или назовите это как-нибудь еще.
Ромен

1
Но по крайней мере выбор «видимого сдвига» основан на легко понятных свойствах отображения (или фрейма) - в отличие от <правильного ответа>, Kкоторый трудно и не интуитивно выбрать.
GoZoner

5

Правильный вопрос: как сравнить очки в Cocoa Touch?

Правильный ответ: CGPointEqualToPoint ().

Другой вопрос: одинаковы ли два вычисленных значения?

Ответ выложен здесь: их нет.

Как проверить, если они близко? Если вы хотите проверить, близки ли они, не используйте CGPointEqualToPoint (). Но не проверяйте, если они близко. Сделайте что-то, что имеет смысл в реальном мире, например, проверьте, находится ли точка за линией или находится ли она внутри сферы.


4

В прошлый раз, когда я проверял стандарт C, не было требования, чтобы операции с плавающей запятой на двойных числах (всего 64 бита, 53-битной мантиссе) были точнее, чем эта точность. Однако некоторые аппаратные средства могут выполнять операции в регистрах с большей точностью, и это требование было истолковано как означающее отсутствие требования очищать биты младших разрядов (помимо точности чисел, загружаемых в регистры). Таким образом, вы можете получить неожиданные результаты сравнений, подобных этому, в зависимости от того, что осталось в регистрах от того, кто спал там последним.

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


1

Вы можете использовать такой код для сравнения с плавающей точкой с нуля:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

Это будет с точностью до 0,1, что достаточно для CGFloat в этом случае.


Приведение к intбез страховки theView.frame.origin.xнаходится в / около того диапазона intприводит к неопределенному поведению (UB) - или в этом случае 1/100 диапазона int.
chux - Восстановить Монику

Там нет абсолютно никаких оснований для преобразования в целое число, как это. Как сказал chux, есть потенциал для UB из значений вне диапазона; и на некоторых архитектурах это будет значительно медленнее, чем просто выполнение вычислений с плавающей запятой. Наконец, умножение на 100 будет сравнивать с точностью до 0,01, а не с 0,1.
Sneftel

0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}


0

Я использую следующую функцию сравнения, чтобы сравнить количество десятичных разрядов:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

-6

Я бы сказал, что правильно объявить каждое число как объект, а затем определить три вещи в этом объекте: 1) оператор равенства. 2) метод setAcceptableDifference. 3) само значение. Оператор равенства возвращает true, если абсолютная разница двух значений меньше значения, установленного как допустимое.

Вы можете создать подкласс для объекта в соответствии с проблемой. Например, круглые металлические стержни от 1 до 2 дюймов могут считаться равными по диаметру, если их диаметры различаются менее чем на 0,0001 дюйма. Таким образом, вы должны вызвать setAcceptableDifference с параметром 0.0001, а затем с уверенностью использовать оператор равенства.


1
Это не хороший ответ. Во-первых, вся «объектная вещь» ничего не делает для решения вашей проблемы. И второе, ваша фактическая реализация «равенства» на самом деле не правильная.
Том Свирли,

3
Том, может, ты снова подумаешь о «предмете». С действительными числами, представленными с высокой точностью, равенство случается редко. Но идея равенства может быть адаптирована, если она вам подходит. Было бы лучше, если бы существовал переопределяемый оператор «примерно равный», но его нет.
Джон Уайт
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.