Странная проблема при сравнении чисел с плавающей точкой в ​​объекте-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 или что-то в этом роде?

Спасибо.


person Dimitris    schedule 23.10.2009    source источник
comment
См. Также «Что должен знать каждый компьютерный ученый об арифметике с плавающей запятой»: docs.sun .com / source / 806-3568 / ncg_goldberg.html.   -  person Curt Nichols    schedule 23.10.2009
comment
Для тех из вас (особенно @alastair), кто работал над улучшением моего ответа, я не уверен, что его можно улучшить. Я согласен, что это было неправильно и, вероятно, опасно. Я удалил это. См. Ответ Джеймса Снука для более глубокого изучения этой нетривиальной проблемы.   -  person Rob Napier    schedule 26.09.2016
comment
@RobNapier Я должен сказать, что я подумал, что стоит оставить ответ с добавленными исправлениями, но я понимаю вашу точку зрения на это. Прошу прощения, если мое правка показалась немного агрессивной - я просто хотел прояснить, в чем проблема.   -  person alastair    schedule 27.09.2016
comment
@alastair Вовсе нет; это помогло избавиться от чего-то, что, по моему мнению, вводило людей в заблуждение (включая меня самого). Я бы предпочел улучшить ответ Джеймса Снука всем, что вы хотите добавить. Ошибки в SO, даже отмеченные неправильными, иногда могут сбивать читателей с толку. Если ваш представитель не позволяет вам видеть удаленные сообщения, и вы хотите скопировать что-то, что вы ранее написали, в свой ответ или ответ Джеймса, я разместил старый текст здесь (а также для всех, кому интересно, о чем мы говорим: D ): gist.github.com/rnapier/78502480e53f526d24f30a14032dea8d)   -  person Rob Napier    schedule 27.09.2016


Ответы (7)


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

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

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

person Georg Schölly    schedule 23.10.2009

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

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

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

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

Хорошая подпрограмма сравнения с плавающей запятой выглядит примерно так:

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

person James Snook    schedule 17.03.2016
comment
Что такое epsilonMultiplier? - person Albert Renshaw; 07.10.2018
comment
@AlbertRenshaw epsilonMultiplier позволяет настроить размер эпсилона. Например, установка значения 1 позволяет двум ближайшим к желаемому результату числам с плавающей запятой считаться равными. Увеличение его увеличивает количество значений, которые будут сравнивать равными. Вы можете рассуждать о размере epsilonMultiplier, основываясь на том, как рассчитывается значение, или экспериментально. - person James Snook; 04.12.2018

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

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

person MarkPowell    schedule 23.10.2009

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

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

Предположим, мы делали это в десятичном формате, где float - это три цифры, а double - шесть, и мы сравниваем с 1/3.

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

person David Thornley    schedule 23.10.2009
comment
Следуя этой мысли в десятичной системе (которая имеет разные точные числа, чем двоичные, но концепция та же), вы обнаружите, что (1/3) * 3! = 1, независимо от того, сколько (конечных) цифр вы выберете. Вот почему выполнение всех ваших вычислений с плавающей запятой или двойным числом на самом деле не решает проблему. - person Rob Napier; 23.10.2009
comment
Правильно. Конечно, вы можете произвольно приблизиться к 1, используя достаточное количество цифр, поэтому проверка на действительно близкое расстояние работает намного лучше, чем проверка на равенство. Настоящая проблема здесь в том, что тесты на равенство с плавающей запятой в целом не работают. - person David Thornley; 24.10.2009
comment
Согласованный. Таким образом, доступно предупреждение (которое я рекомендую включить), чтобы вы случайно не использовали равенство с плавающей запятой. - person Rob Napier; 24.10.2009

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

0.0001100110011001100110011001100110011001100110011...

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

person epatel    schedule 23.10.2009

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

person Lou Franco    schedule 23.10.2009
comment
sizeof показывает, что 0,1 - это двойное значение. Все еще очень странно, что вы не получаете равенства, когда оба имеют значение 0,10000, не так ли? - person Dimitris; 23.10.2009
comment
@ Димитрис, это не так уж и странно. См. Ответ @MarkPowell. - person Carl Norum; 23.10.2009
comment
@ Димитрис: Нет, это не очень странно. 0.1 не может быть точно представлен в двоичном формате. Как сказал Лу, не рассчитывайте на равенство чисел с плавающей запятой. Всегда. Убедитесь, что числа находятся в пределах небольшого поля друг от друга (например, (a - m < b) && (a + m > b)). - person Chuck; 23.10.2009

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

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

return [numberA isEqualToString: numberB];
person Henry Sou    schedule 18.12.2016