Странная проблема сравнения поплавков в target-C - PullRequest
23 голосов
/ 23 октября 2009

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

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 или как-то еще?

Спасибо.

Ответы [ 7 ]

30 голосов
/ 23 октября 2009

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

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

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

6 голосов
/ 17 марта 2016

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

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

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

4 голосов
/ 23 октября 2009

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

0.0001100110011001100110011001100110011001100110011...

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

4 голосов
/ 23 октября 2009

В 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 октября 2009

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

Статья IEEE с плавающей точкой в ​​Википедии может помочь вам понять это различие.

1 голос
/ 23 октября 2009

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

0 голосов
/ 18 декабря 2016

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

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

return [numberA isEqualToString: numberB];
...