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

355

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

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

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

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

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

Теги:
floating-point
floating-accuracy

10 ответов

468
Лучший ответ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

а также замените DBL_MIN на использование удвоений.

  • 24
    fabs(x+y) проблематичен, если x и y (могут) иметь разные знаки. Тем не менее, хороший ответ против потока культовых сравнений.
  • 25
    Если x и y имеют разные знаки, это не проблема. Правая часть будет «слишком маленькой», но поскольку x и y имеют разные знаки, они все равно не должны сравниваться. (Если они не настолько малы, чтобы быть ненормальными, но тогда второй случай ловит это)
Показать ещё 15 комментариев
34

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

Чтобы немного разъяснить, вычисление, такое как:

areal = 0.0

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

areal = 1.386 - 2.1*(0.66)

не может.

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

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

Для Angry Birds, не так опасно.

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

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

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

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

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

13

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

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

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

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

10

[ "Правильный ответ" замалчивает выбор K. Выбор K заканчивается как раз как ad-hoc при выборе 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 a в несколько раз больше, чем говорят ваши расчеты)

Итак, нам нужно K. Если получение K более сложное, менее интуитивное, чем выбор my 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

Ah, поэтому K должен быть 1e16 или больше, если я хочу, чтобы 1e-13 был "нолем".

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

  • Сделайте простой расчет epsilon, используя свое техническое решение для значения "epsilon", как я уже сказал. Если вы делаете графику, а "ноль" означает "видимое изменение", чем проверяйте свои визуальные объекты (изображения и т.д.) И судите о том, что такое эпсилон.
  • Не пытайтесь вычислять с плавающей запятой, пока не прочитаете ссылку ответа на беспошлинный культ (и не получите ваш кандидат в процессе), а затем используйте неинтуитивное суждение, чтобы выбрать K.
  • 10
    Одним из аспектов независимости от разрешения является то, что вы не можете точно сказать, что такое «видимый сдвиг» во время компиляции. То, что невидимо на экране Super-HD, вполне может быть очевидно на экране с крошечной попкой. Надо хотя бы сделать это функцией размера экрана. Или назовите это как-нибудь еще.
  • 1
    Но по крайней мере выбор «видимого сдвига» основан на легко понимаемых свойствах отображения (или фрейма) - в отличие от <правильного ответа> K который трудно и не интуитивно выбрать.
5

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

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

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

Ответ отправлен здесь: Это не так.

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

4

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

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

1

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

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

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

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

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

Исправьте их сначала с этим:

value = [[NSString stringWithFormat:@"%g", value] floatValue];

Тогда свободно сравнивайте как обычно


Примеры журналов:

(lldb) po [NSString stringWithFormat:@"%g", value]
40000

(lldb) po value
40000.0273

(lldb) po [NSString stringWithFormat:@"%g", max]
30000.7

(lldb) po max
30000.6992

Macro:

#define fixFloat(__value) ([[NSString stringWithFormat:@"%g", (__value)] floatValue])

-6

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню