Странная проблема сравнения поплавков в target-C

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

if (self.scroller.currentValue <= 0.1) {
}

где currentValue - это свойство с плавающей точкой.

Однако, когда у меня есть равенство и self.scroller.currentValue = 0.1оператор if не выполняется, а код не выполняется! Я обнаружил, что могу исправить это, применив 0.1 к float. Нравится:

if (self.scroller.currentValue <= (float)0.1) {
}

Это отлично работает.

Кто-нибудь может мне объяснить, почему это происходит? По умолчанию 0.1 определяется как double или как?

Спасибо.

23.10.2009 16:25:52
См. Также «Что должен знать каждый компьютерщик об арифметике с плавающей точкой»: docs.sun.com/source/806-3568/ncg_goldberg.html
Curt Nichols 23.10.2009 19:08:53
Для тех из вас (особенно @alastair), которые работали над улучшением моего ответа, я не уверен, что его можно улучшить. Я согласен, что это было неправильно и, вероятно, опасно. Я удалил это. Пожалуйста, смотрите ответ Джеймса Снука для более глубокого исследования этой нетривиальной проблемы.
Rob Napier 26.09.2016 17:08:14
@RobNapier Я должен сказать, что я думал, что стоит сохранить ответ с добавленными исправлениями, но я понимаю вашу точку зрения на это. Извините, если мое редактирование показалось немного агрессивным - я просто хотел прояснить, в чем проблема.
alastair 27.09.2016 13:38:21
@alastair Совсем нет; это помогло избавиться от того, что, как мне кажется, вводило в заблуждение людей (в том числе и меня). Я бы предпочел улучшить ответ Джеймса Снука с помощью всего, что вы хотели бы добавить. Неправильное отношение к SO, даже помеченное как неправильное, иногда может сбить с толку читателей. Если ваш представитель не позволяет вам видеть удаленные сообщения, и вы хотите скопировать что-то, что вы ранее написали, в свой собственный ответ или ответ Джеймса, я разместил здесь старый текст (а также для всех, кому интересно, о чем мы говорим: D ): gist.github.com/rnapier/78502480e53f526d24f30a14032dea8d )
Rob Napier 27.09.2016 14:47:39
7 ОТВЕТОВ
РЕШЕНИЕ

Я считаю, не найдя стандарт , который говорит так, что при сравнении floatк отливают к перед сравнением. Числа с плавающей точкой без модификатора считаются в C.doublefloatdoubledouble

Тем не менее, в C нет точного представления 0,1 в числах с плавающей и двойной. Теперь использование float дает небольшую ошибку. Использование двойного дает вам еще меньшую ошибку. Проблема сейчас в том, что, передавая floata, doubleвы переносите большую ошибку float. Конечно, теперь они не сравнятся.

Вместо использования (float)0.1вы можете использовать, 0.1fчто немного приятнее для чтения.

30
23.10.2009 16:57:53

Как правило, на любом языке вы не можете рассчитывать на равенство типов типа float. В вашем случае, поскольку у вас больше контроля, похоже, что 0.1 не является плавающим по умолчанию. Вы, вероятно, могли бы выяснить это с помощью sizeof (0.1) (вместо sizeof (self.scroller.currentValue).

1
23.10.2009 16:29:15
sizeof показывает, что 0,1 является двойным. Все еще очень странно, что вы не получаете равенства, когда оба имеют значение 0.10000, не так ли?
Dimitris 23.10.2009 16:39:01
@Dimitris, это не так странно. Смотрите ответ @ MarkPowell.
Carl Norum 23.10.2009 16:59:46
@Dimitris: Нет, это не очень странно. 0.1 не является точно представимым в двоичном виде. Как сказал Лу, не рассчитывайте на равенство в числах с плавающей запятой. Когда-либо. Проверьте, находятся ли числа в некотором крошечном поле друг от друга (например, (a - m < b) && (a + m > b)).
Chuck 23.10.2009 17:02:53

Двойные числа и числа с плавающей запятой имеют различные значения для хранилища мантиссы в двоичном формате (число с плавающей запятой 23 бита, двойное 54) Они почти никогда не будут равны.

Статья IEEE Float Point в Википедии может помочь вам понять это различие.

4
23.10.2009 16:33:23

В C литерал с плавающей точкой, такой как 0.1, является двойным, а не плавающим. Поскольку типы сравниваемых элементов данных различны, сравнение выполняется в более точном виде (double). Во всех реализациях, о которых я знаю, float имеет более короткое представление, чем double (обычно выражается примерно как 6 против 14 десятичных знаков). Кроме того, арифметика в двоичном, а 1/10 не имеет точного представления в двоичном.

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

Предположим, мы делали это в десятичном формате, с плавающей точкой, состоящей из трех цифр, и двойной, равной шести, и мы сравнивали с 1/3.

У нас есть сохраненное значение с плавающей запятой, равное 0,333. Мы сравниваем его с двойным значением 0,333333. Мы конвертируем число с плавающей запятой в 0,333 в удвоенное значение 0,333000 и находим другое

4
23.10.2009 17:31:47
Следуя этой мысли в десятичном виде (которая имеет жесткие числа, отличные от двоичных, но концепция та же самая), вы обнаружите, что (1/3) * 3! = 1, независимо от того, сколько (конечных) цифр вы выберете. Вот почему выполнение всей математики с плавающей или двойной не решает проблему.
Rob Napier 23.10.2009 18:30:27
Правильно. Конечно, вы можете получить произвольно близкое к 1, используя достаточное количество цифр, поэтому тестирование для получения действительно близкого работает намного лучше, чем тестирование на равенство. Настоящая проблема здесь заключается в том, что тесты на равенство с плавающей точкой в ​​общем случае не работают.
David Thornley 23.10.2009 20:00:06
Согласовано. Таким образом, доступно предупреждение (которое я рекомендую включить), чтобы предотвратить случайное использование равенства с плавающей точкой.
Rob Napier 24.10.2009 16:37:27

0.1 на самом деле очень сложное значение для хранения двоичного файла. В базе 2 1/10 - бесконечно повторяющаяся дробь

0.0001100110011001100110011001100110011001100110011...

Как уже отмечалось, сравнение должно проводиться с постоянной одинаковой точности.

4
23.10.2009 17:37:42

Проблема в том, что, как вы предположили в своем вопросе, вы сравниваете число с плавающей запятой.

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

Чтобы выбрать хороший эпсилон, нужно немного разбираться в числах с плавающей запятой. Числа с плавающей точкой работают аналогично представлению числа для данного числа значащих цифр. Если мы вычислим до 5 значащих цифр, и ваши вычисления приведут к тому, что последняя цифра результата будет неправильной, тогда 1.2345 будет иметь ошибку + -0.0001, тогда как 1234500 будет иметь ошибку + -100. Если вы всегда основываете свою погрешность на значении 1,2345, тогда ваша процедура сравнения будет идентична ==для всех значений, больших 10 (при использовании десятичного числа). Это хуже в двоичном коде, все значения больше 2. Это означает, что эпсилон, который мы выбираем, должен быть относительно размера поплавков, которые мы сравниваем.

FLT_EPSILON - это промежуток между 1 и следующим ближайшим числом с плавающей точкой. Это означает, что может быть хорошим эпсилоном выбрать, будет ли ваше число между 1 и 2, но если ваше значение больше 2, использование этого эпсилона бессмысленно, потому что разрыв между 2 и следующим ближайшим числом с плавающей запятой больше, чем эпсилон. Таким образом, мы должны выбрать эпсилон относительно размера наших поплавков (так как ошибка в расчете относительно размера наших поплавков).

Хорошая (ish) процедура сравнения с плавающей точкой выглядит примерно так:

bool compareNearlyEqual (float a, float b, unsigned epsilonMultiplier)       
{
  float epsilon;
  /* May as well do the easy check first. */
  if (a == b)
    return true;

  if (a > b) {
    epsilon = scalbnf(1.0f, ilogb(a)) * FLT_EPSILON * epsilonMultiplier;
  } else {
    epsilon = scalbnf(1.0, ilogb(b)) * FLT_EPSILON * epsilonMultiplier;
  }

  return fabs (a - b) <= epsilon;
}

Эта процедура сравнения сравнивает числа с плавающей запятой относительно размера самого большого переданного объекта с плавающей точкой. scalbnf(1.0f, ilogb(a)) * FLT_EPSILONНаходит промежуток между aследующим ближайшим числом с плавающей точкой. Затем это умножается на epsilonMultiplier, так что размер разницы можно регулировать в зависимости от того, насколько неточным будет результат вычисления.

Вы можете сделать простую compareLessThanпроцедуру, как это:

bool compareLessThan (float a, float b, unsigned epsilonMultiplier)
{
  if (compareNearlyEqual (a, b, epsilonMultiplier)
    return false;

  return a < b;
}

Вы также можете написать очень похожую compareGreaterThanфункцию.

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

Иногда получаемые неточности не будут зависеть от размера результата вычисления, но будут зависеть от значений, которые вы вводите в расчет. Например sin(1.0f + (float)(200 * M_PI)), даст гораздо менее точный результат, чем sin(1.0f)(результаты должны быть идентичны). В этом случае ваша процедура сравнения должна будет посмотреть на число, которое вы положили в расчет, чтобы узнать предел погрешности ответа.

6
17.03.2016 10:55:48
Что такое epsilonMultiplier?
Albert Renshaw 6.10.2018 22:42:19
@AlbertRenshaw epsilonMultiplierпозволяет настроить размер эпсилона. Например, установив его так, чтобы 1два ближайших числа с желаемым результатом считались равными. Увеличение его увеличивает количество значений, которые будут сравниваться равными. Вы можете рассуждать о размере, epsilonMultiplierосновываясь на том, как рассчитывается значение, или экспериментально.
James Snook 4.12.2018 16:37:40

Преобразуйте его в строку, затем сравните:

NSString* numberA = [NSString stringWithFormat:@"%.6f", a];
NSString* numberB = [NSString stringWithFormat:@"%.6f", b];

return [numberA isEqualToString: numberB];
-1
18.12.2016 13:36:55