[McSeem2] Про сравнение double

McSeem2 McSeem2
Здравствуйте, gandjustas, Вы писали:

G>Нет. Вообще прямое сравнение чисел с плавающией запятой, вида (a==b) может дать неправильный результат

UN>Не 0.0000001, а EPS — это значение определено в CRT =)

. . .ну и т.д.

Так. Тут начался беспредел с ужасами про сравнение плавающей точки на равенство. Требуется ликбез. Сравнивать плавающие числа (для педантов — значения переменных типа float или double) вполне можно и даже нужно. Но надо понимать сущность этой плавающей точки. А сущность заключается в том, что числа с фиксированной точкой (целые — это частный случай чисел с фиксированной точкой) имеют абсолютное значение погрешности, в отличие от чисел с плавающей точкой, где значение погрешности находится в прямой пропорциональности от модуля числа. Во всяком случае:
double a=3.1415;
double b=a;
if(a == b)
{
   . . .
}

Вот если этот if не сработает, то это означает, что компьютер сломался, а процессор издох.

Другое дело, когда числа вычислены разными способами — одно через синус, а другое — через экспоненту. Здесь действительно проверка на равенство скорее всего не сработает. Так же, как и не сработает сравнение с константой. Но это же относится и к целым числам, если скажем, мы нормализуем значения от 0...1 к 0...1000000000. То есть, не имеет значения, плавающие это числа или целые. В определенных ситуациях сравнивать их на строгое равенство нельзя. В этих ситуациях надо использовать некую Epsilon. И вот здесь-то и вылезает наружу вся безграмотность. Что такое DBL_EPSILON? — а это вот что. Это минимальное значение, которое при прибавлении его к единице, меняет значение этой единицы. Понимаете? — к единице! Строгой единице, числу 1.0 и ни к какому другому. Поэтому сравнивать числа с плавающей точкой на +/- DBL_EPSILON совершенно бессмысленно. Это сравнение выдает всю глубину невежества и неспособности думать мозгом. Факты таковы — плавающие числа больше 2.0 эту DBL_EPSILON просто не ощущают. Им что прибавляй ее, что что нет — ничего не меняет. Для этих чисел DBL_EPSILON является строгим нулем и просто не существует. В то же время, DBL_EPSILON имеет значение порядка 1e-16. Что это значит? А это значит, что числа в диапазоне Планковских масштабов (типа 1e-34) с точки зрения этой DBL_EPSILON будут все равны. То есть, эта 1e-16 становится слоном в посудной лавке. А ведь постоянная Планка ничуть не хуже скорости света — для этого собственно и были придуманы числа с плавающей точкой, чтобы отображать большие диапазоны значений с неким фиксированным количеством знаков.

Так для чего же все-таки нужна эта самая DBL_EPSILON (ну или FLT_EPSILON)? Нужна-ли? — нужна! Есть ситуации, когда действительно надо сравнивать числа в неком допустимом интервале. В каком? — А вот это как раз и зависит от абсолютного значения чисел и сущности вычислений. Короче говоря, надо эту Epsilon умножить на значение числа. А поскольку у нас два числа, то все усложняется — какое из них брать. То есть, корректное сравнение выглядит так:
if (fabs(a-b) <= DBL_EPSILON * fmax(fabs(a),fabs(b)))
{
  . . .Числа равны с относительной точностью DBL_EPSILON
}

Дорого? Да, дорого, а все остальное неправильно, такие дела. Но и это тоже неправильно! Дело в том, что этот DBL_EPSILON определяет разницу в 1 (один!) значащий бит экспоненты в приложении к числу 1.0. На практике такой разницы не встречается — числа либо строго равны, либо могут различаться больше чем на один значащий бит. Поэтому надо брать что-то типа 16*DBL_EPSILON, чтобы игнрорировать разницу в 4 младших бита (или примерно полторы последние значащие десятичные цифры из примерно 16 имеющихся).

Конечно же, есть случаи, когда диапазон чисел более-менее известен и предсказуем. Скажем, 0...1000. В этом случае, для сравнения на приблизительное равенство можно взять константу, типа 1000*16*DBL_EPSILON. Но надо иметь в виду, что такое сравнение фактически превращает всю идею плавающей точки в фиксированную точку (догадайтесь, почему).

Я вообще поражен уровню невежества — даже в весьма грамотной библиотеке GPC by Alan Murta используется тупое сравнение с константной Epsilon. На диапазонах экранных координат это все равно, что сравнение на строгое равенство, а на 1e-20 алгоритм вообще перестает работать.

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

Вроде бы все сказал. Дополняйте.
AstroMan
AstroMan memcmp и float
01.09.2007 09:39
Здравствуйте, McSeem2, Вы писали:

MS>И пожалуйста, имейте смелость высмеять от меня в лицо того университетского преподавателя, который скажет вам, что сравнивать плавающие числа на равенство нельзя. Такое высказывание можно простить в школе, на первом уроке информатики, но никак не в универе.


Можно сказать проще, что все стандартные эпсилоны относятся только к мантиссе числа. В большинстве задач надо вводить погрешность из проблемной области (например, 0.001 см для разметки текста на странице или 1.0E+30 г для массы звезд).
McSeem2
McSeem2
02.09.2007 09:01
Здравствуйте, AstroMan, Вы писали:

AM>Можно сказать проще, что все стандартные эпсилоны относятся только к мантиссе числа. В большинстве задач надо вводить погрешность из проблемной области (например, 0.001 см для разметки текста на странице или 1.0E+30 г для массы звезд).


Я специально написал такой развернутый ответ, за еще и "с эмоциями". Сказать проще конечно можно, но это увы не наглядно и часто не доходит.
Шахтер
Шахтер memcmp и float
02.09.2007 01:49
Здравствуйте, McSeem2, Вы писали:

Сравнивать плавающие числа или не сравнивать и как именно -- зависит исключительно о решаемой задачи. Вне конкретного контекста обсуждать можно лишь свойства примитивных опрераций -- в данном случае операции сравнения.
McSeem2
McSeem2
02.09.2007 08:58
Здравствуйте, Шахтер, Вы писали:

Ш>Сравнивать плавающие числа или не сравнивать и как именно -- зависит исключительно о решаемой задачи. Вне конкретного контекста обсуждать можно лишь свойства примитивных опрераций -- в данном случае операции сравнения.


Безусловно. Но надо иметь хотя бы начальную печку, от которой танцевать. А то так и будут писать ахинею типа if(fabs(a-b) <= DBL_EPSILON)...
netch80
netch80
16.12.2007 07:59
Здравствуйте, McSeem2, Вы писали:

MS>Здравствуйте, Шахтер, Вы писали:


Ш>>Сравнивать плавающие числа или не сравнивать и как именно -- зависит исключительно о решаемой задачи. Вне конкретного контекста обсуждать можно лишь свойства примитивных опрераций -- в данном случае операции сравнения.


MS>Безусловно. Но надо иметь хотя бы начальную печку, от которой танцевать. А то так и будут писать ахинею типа if(fabs(a-b) <= DBL_EPSILON)...


Вот с этим полностью согласен. (Пишу это явно на всякий случай)
netch80
netch80 memcmp и float
16.12.2007 07:58
Здравствуйте, McSeem2, Вы писали:

MS>И пожалуйста, имейте смелость высмеять от меня в лицо того университетского преподавателя, который скажет вам, что сравнивать плавающие числа на равенство нельзя. Такое высказывание можно простить в школе, на первом уроке информатики, но никак не в универе.


Я всё-таки на это возражу Дело в том, что в вычислительном алгоритме ситуаций, которые достаточно просто сводятся к

  double b = a;
  if (a != b) { шеф, усё пропало! }


крайне мало — я бы сказал, что их вообще не бывает.

А вот ситуаций, когда пишут код вида

  for(double x = 0.0; x <= 20.0; x += 0.1) {
    ...
  }


много — фактически, каждому хотя бы раз приходилось писать такое (и потом удивляться результату — а куда делась последняя итерация?)

Именно поэтому устанавливать принцип "не сравнивайте на точное равенство! всегда сравнивайте величину разницы" вместе с некоторыми простыми советами типа "замените x<=20.0 на x<=20.5" имеет право на жизнь и тем более в университете. А ещё есть конверсии, когда double где-то был перенесен во float, а потом обратно... Найти случаи, когда надо сравнивать более просто, грамотный программист сможет — а если не сможет, ему вообще в вычислительной математике делать нечего. А вот сделать, чтобы правильное сравнение вошло "в плоть и кровь" — преподаватель таки должен.

По поводу вот этого:

MS> Конечно же, есть случаи, когда диапазон чисел более-менее известен и предсказуем. Скажем, 0...1000. В этом случае, для сравнения на приблизительное равенство можно взять константу, типа 1000*16*DBL_EPSILON. Но надо иметь в виду, что такое сравнение фактически превращает всю идею плавающей точки в фиксированную точку (догадайтесь, почему).


До определённой степени та же "фиксированная точка" тут совсем неплоха. По крайней мере при сравнении. Только, наверно, не при 0...1000, а хотя бы при 10...1000. Общих рецептов всё равно не будет, в вычислительной математике огромное количество тонкостей, зависящих и от специфики исходной задачи... я когда-то наигрался с этим.
McSeem2
McSeem2
17.12.2007 04:50
Здравствуйте, netch80, Вы писали:

N>Я всё-таки на это возражу Дело в том, что в вычислительном алгоритме ситуаций, которые достаточно просто сводятся к


N>
N>  double b = a;
N>  if (a != b) { шеф, усё пропало! }
N>


N>крайне мало — я бы сказал, что их вообще не бывает.


Бывает и довольно много, по крайней мере в моей практике. Например, восстановить связность графа по набору ребер и координатам их концов. Вполне практическая задача, необходимая для растеризации данных из Flash. Вообще, в вычислительной геометрии встречается сплошь и рядом.

N>А вот ситуаций, когда пишут код вида


N>
N>  for(double x = 0.0; x <= 20.0; x += 0.1) {
N>    ...
N>  }
N>


N>много — фактически, каждому хотя бы раз приходилось писать такое (и потом удивляться результату — а куда делась последняя итерация?)


Это несколько другой эффект, связанный с невозможностью точного представления десятичных дробей в двоичном виде. Числа 0.1 в двоичном виде не существует. Вышеуказанный цикл может сработать неправильно даже с фиксированной точкой. Здесь главным условием является наличие десятичной дроби (причем, не всякой), представленной в двоичном виде. Например, при обработке в BCD все сработает нормально. В случае с таким циклом надо именно что разъяснить студентам этот побочный эффект на как можно более ранних стадиях. Например:

  for(double x = 0.0; x <= 20.0; x += 0.1) {  // неправильно
    ...
  }

  for(double x = 0.0; x <= 200.0; x += 1.0) {  // правильно
    ...
  }

  for(double x = 0.0; x <= 100.0; x += 0.5) {  // правильно
    ...
  }


Другими словами, требуется обеспечить понимание, что некоторые десятичные дроби не имеют точного представления в двоичном виде.

Так называемые практические советы, типа "x <= 20.05" опасны, поскольку могут только запутать и сбить с толку. И главное — они лечат симптом, а не болезнь, еще больше отдаляя понимание сущности. Вот после того, как наступило понимание, можно дать подобный совет, но не раньше. Но при этом и надобность в подобном совете отпадает — человек уже понимает сущность и сам сообразит что к чему.