How dangerous is it to compare floating point values?

Я знаю UIKit использования CGFloat because of the resolution independent coordinate system.

But every time I want to check if for example frame.origin.x является 0 it makes me feel sick:

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

не CGFloat vulnerable to false positives when comparing with ==, <=, >=, <, >? It is a floating point and they have unprecision problems: 0.0000000000041 например.

Является Objective-C handling this internally when comparing or can it happen that a origin.x which reads as zero does not compare to 0 как правда?

12 ответов

Решение

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

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

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

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

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

  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 заdoubleс или LDBL_EPSILON за long doubleс) и K константа, которую вы выбираете так, что накопленная ошибка ваших вычислений определенно ограничена K единиц в последнем месте (и если вы не уверены, что правильно рассчитали границы ошибок, сделайте K в несколько раз больше, чем должно быть в ваших расчетах).

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

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

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

Поскольку 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 не так уж и опасно.

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

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

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

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

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

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

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

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

"Правильный ответ" закрывает выбор 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,

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

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

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

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

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

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

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

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

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

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

Еще одна проблема, которую, возможно, необходимо иметь в виду, заключается в том, что разные реализации делают вещи по-разному. Одним из примеров этого, с которым я очень хорошо знаком, являются блоки FP на Sony Playstation 2. Они имеют значительные расхождения по сравнению с аппаратным обеспечением IEEE FP в любом устройстве X86. В цитируемой статье упоминается полное отсутствие поддержки inf и NaN, и становится все хуже.

Менее известно то, что я узнал как ошибку «умножения на один бит». Для определенных значенийfloat x:

          y = x * 1.0;
    assert(y == x);

провалит утверждение. В общем случае иногда, но не всегда, результат умножения FP на Playstation 2 имел мантисса, которая была на один бит меньше, чем эквивалентная мантисса IEEE.

Я хочу сказать, что вы не должны предполагать, что перенос кода FP с одной платформы на другую приведет к тем же результатам. Любая данная платформа внутренне непротиворечива , поскольку результаты не меняются на этой платформе, просто они могут не совпадать с другой платформой. Например, CPython на X86 использует 64-битные двойные числа для представления чисел с плавающей запятой, в то время как CircuitPython на Cortex MO должен использовать программный FP и использует только 32-битные числа с плавающей запятой. Излишне говорить, что это приведет к расхождениям.

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

Playstation является зарегистрированным товарным знаком Sony Corporation.

-(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;

}

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

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
}

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

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

Другие вопросы по тегам