Вопрос по objective-c, ios, floating-point, floating-accuracy, c – Насколько опасно сравнивать значения с плавающей запятой?

374

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

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

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

Isn & APOS; тCGFloat уязвимы для ложных срабатываний при сравнении с==, <=, >=, <, >? It is a floating point and they have unprecision problems: 0.0000000000041 например.

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

Ваш Ответ

10   ответов
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;

446

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

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

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

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

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

The fact that lots of values you may write in the source, or read in with scanf or strtod, do not exist as floating point values and get silently converted to the nearest approximation. This is what demon9733's answer was talking about.

The fact that many results get rounded due to not having enough precision to represent the actual result. An easy example where you can see this is adding x = 0x1fffffe and y = 1 as floats. Here, x has 24 bits of precision in the mantissa (ok) and y has just 1 bit, but when you add them, their bits are not in overlapping places, and the result would need 25 bits of precision. Instead, it gets rounded (to 0x2000000 in the default rounding mode).

The fact that many results get rounded due to needing infinitely many places for the correct value. This includes both rational results like 1/3 (which you're familiar with from decimal where it takes infinitely many places) but also 1/10 (which also takes infinitely many places in binary, since 5 is not a power of 2), as well as irrational results like the square root of anything that's not a perfect square.

Double rounding. On some systems (particularly x86), floating point expressions are evaluated in higher precision than their nominal types. This means that when one of the above types of rounding happens, you'll get two rounding steps, first a rounding of the result to the higher-precision type, then a rounding to the final type. As an example, consider what happens in decimal if you round 1.49 to an integer (1), versus what happens if you first round it to one decimal place (1.5) then round that result to an integer (2). This is actually one of the nastiest areas to deal with in floating point, since the behaviour of the compiler (especially for buggy, non-conforming compilers like GCC) is unpredictable.

Transcendental functions (trig, exp, log, etc.) are not specified to have correctly rounded results; the result is just specified to be correct within one unit in the last place of precision (usually referred to as 1ulp).

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

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

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 если использовать удваивается.

Мне любопытно ваше утверждение: "особенно для глючных несовместимых компиляторов, таких как GCC". Действительно ли GCC глючит и не соответствует?
fabs(x+y) проблематично, еслиx а такжеy (может) иметь другой знак. Тем не менее, хороший ответ против потока культовых сравнений.
@Daniel: +1 в любом случае к вашему комментарию за использование слова "cargo-cult". :-)
"First of all, floating point values are not "random" in their behavior. Exact comparison can and does make sense in plenty of real-world usages." - Всего два предложения и уже заработал +1! Это одно из самых тревожных заблуждений людей при работе с плавающей запятой.
Еслиx а такжеy имеют другой знак, это не проблема. Правая сторона будет "слишком маленькой", но так какx а такжеy имеют разные знаки, они не должны сравниваться равными в любом случае. (Если они не настолько малы, чтобы быть ненормальными, но тогда второй случай ловит это)
10

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 is a constant you choose such that the accumulated error of your computations is definitely bounded by K units in the last place (and if you're not sure you got the error bound calculation right, make K a few times bigger than what your calculations say it should be)

Итак, нам нужно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");
    }
}
[email protected]$ gcc -o test test.c
[email protected]$ ./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 был "нулем".

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

Do a simple epsilon computation using your engineering judgement for the value of 'epsilon', as I've suggested. If you are doing graphics and 'zero' is meant to be a 'visible change' than examine your visual assets (images, etc) and judge what epsilon can be. Don't attempt any floating point computations until you've read the non-cargo-cult answer's reference (and gotten your Ph.D in the process) and then use your non-intuitive judgement to select K.
Одним из аспектов независимости от разрешения является то, что вы не можете точно сказать, что такое «видимый сдвиг». находится во время компиляции. То, что невидимо на экране Super-HD, вполне может быть очевидно на экране с крошечной попкой. Надо хотя бы сделать это функцией размера экрана. Или назовите это как-нибудь еще.
Но, по крайней мере, выбирая «видимый сдвиг» основан на легко понятных свойствах отображения (или фрейма) - в отличие от & lt; правильного ответа & gt;K что сложно и не интуитивно понятно выбрать.
4

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

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

20

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

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

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

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

1

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

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

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

Нет абсолютно никакой причины преобразовывать в целое число, как это. Как сказал chux, существует потенциал для UB из значений вне диапазона; и на некоторых архитектурах это будет значительно медленнее, чем просто выполнение вычислений с плавающей запятой. Наконец, умножение на 100 будет сравнивать с точностью до 0,01, а не с 0,1.
Кастинг вint без страховкиtheView.frame.origin.x находится в / около этого диапазонаint приводит к неопределенному поведению (UB) - или в этом случае 1/100 диапазонаint.
5

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

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

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

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

13

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

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

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

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

-6

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

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

Это не хороший ответ. Во-первых, целая «вещь объекта» ничего не делает, чтобы решить вашу проблему. И, во-вторых, ваша реальная реализация «равенства» на самом деле не является правильным.
Том, может быть, ты снова подумаешь о "объекте"? С действительными числами, представленными с высокой точностью, равенство случается редко. Но одинidea равенства могут быть адаптированы, если вам это подходит. Было бы лучше, если бы был переопределенный «приблизительно равный». оператор, но нет.
34

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

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

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

Re: Angry Birds: В финансовом плане ошибка, из-за которой у 10 миллионов геймеров не будет хлюпа свиней, может иметь свои последствия.
На самом деле,1.30 - 2*(0.65) является прекрасным примером выражения, которое, очевидно, оценивается в 0.0, если ваш компилятор реализует IEEE 754, потому что числа, представленные как0.65 а также1.30 имеют одинаковые значения, и умножение на два, очевидно, является точным.
Все еще получаю повторение от этого, поэтому я изменил второй пример фрагмента.

Похожие вопросы