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

Я использую Delphi XE2 Update 3. Существуют проблемы с точностью даже для самых простых чисел с плавающей запятой (например, 3.7). Учитывая этот код (32-битное консольное приложение):

program Project1;

{$APPTYPE CONSOLE}
{$R *.res}

uses System.SysUtils;

var s: Single; d: Double; x: Extended;
begin
  Write('Size of Single  -----  ');  Writeln(SizeOf(Single));
  Write('Size of Double  -----  ');  Writeln(SizeOf(Double));
  Write('Size of Extended  ---  ');  Writeln(SizeOf(Extended));  Writeln;

  s := 3.7;  d := 3.7;  x := 3.7;

  Write('"s" is ');                  Writeln(s);
  Write('"d" is ');                  Writeln(d);
  Write('"x" is ');                  Writeln(x);                 Writeln;

  Writeln('Single Comparison');
  Write('"s > 3.7"  is  ');          Writeln(s > 3.7);
  Write('"s = 3.7"  is  ');          Writeln(s = 3.7);
  Write('"s < 3.7"  is  ');          Writeln(s < 3.7);           Writeln;

  Writeln('Double Comparison');
  Write('"d > 3.7"  is  ');          Writeln(d > 3.7);
  Write('"d = 3.7"  is  ');          Writeln(d = 3.7);
  Write('"d < 3.7"  is  ');          Writeln(d < 3.7);           Writeln;

  Writeln('Extended Comparison');
  Write('"x > 3.7"  is  ');          Writeln(x > 3.7);
  Write('"x = 3.7"  is  ');          Writeln(x = 3.7);
  Write('"x < 3.7"  is  ');          Writeln(x < 3.7);           Readln;
end.

Я получаю этот вывод:

Size of Single  -----  4
Size of Double  -----  8
Size of Extended  ---  10

"s" is  3.70000004768372E+0000
"d" is  3.70000000000000E+0000
"x" is  3.70000000000000E+0000

Single Comparison
"s > 3.7"  is  TRUE
"s = 3.7"  is  FALSE
"s < 3.7"  is  FALSE

Double Comparison
"d > 3.7"  is  TRUE
"d = 3.7"  is  FALSE
"d < 3.7"  is  FALSE

Extended Comparison
"x > 3.7"  is  FALSE
"x = 3.7"  is  TRUE
"x < 3.7"  is  FALSE

Вы можете видеть, что extended — единственный тип, который вычисляется правильно. Я думал, что точность — это проблема только при использовании сложных чисел с плавающей запятой, таких как 3.14159265358979323846, а не таких простых, как 3.7. Проблема при использовании single имеет смысл. Но почему double не работает?


person James L.    schedule 14.05.2014    source источник
comment
Это должно дать вам short answer. Вот человек readable article и detailed story.   -  person TLama    schedule 15.05.2014
comment
3.7 не простой. 3,75 есть. Или 3,71875 ф.и. если вы хотите быть немного ближе, или 3,69921875...   -  person Sertac Akyuz    schedule 15.05.2014
comment
Не могу поверить, что это не дубликат. Я сделал быстрый поиск, но ничего не нашел.   -  person Graymatter    schedule 15.05.2014
comment
Всем отличные комментарии и спасибо за статьи.   -  person James L.    schedule 15.05.2014
comment
Пожалуйста, не думайте, что вы можете просто использовать расширенный и вдруг все будет хорошо   -  person David Heffernan    schedule 15.05.2014
comment
@ Дэвид - Согласен. Все комментарии и ответы помогли мне лучше понять нюансы чисел и операций с плавающей запятой. Extended, конечно, не волшебное решение.   -  person James L.    schedule 15.05.2014
comment
На самом деле расширенный обычно просто приводит к более медленным программам.   -  person David Heffernan    schedule 15.05.2014
comment
@DavidHeffernan, иногда требуется повышенная точность, несмотря на то, что это приводит к замедлению выполнения. Есть много численных методов, которые выигрывают от дополнительной точности.   -  person LU RD    schedule 15.05.2014
comment
@LURD Я не спорю, что иногда это полезно. Хотя я думаю, что такие случаи исключительно редки.   -  person David Heffernan    schedule 15.05.2014
comment
@DavidHeffernan, возможно, редко, но все же они нужны большинству моих приложений.   -  person LU RD    schedule 15.05.2014
comment
@LURD Мне любопытно, почему это так. Когда я портировал на 64 бит, у меня было несколько сценариев, где я использовал расширенный. Я проанализировал их и просто переделал алгоритм так, чтобы он работал в двойном размере. Что такого в ваших алгоритмах, которые требуют расширения?   -  person David Heffernan    schedule 15.05.2014
comment
@DavidHeffernan, в первую очередь исключение Гаусса-Джордана. В некоторых случаях он может быть нестабильным с двойной точностью.   -  person LU RD    schedule 15.05.2014
comment
@LURD На мой взгляд, способ справиться с этим - избежать использования Гаусса-Джордана и использовать алгоритм, который не страдает этой нестабильностью. Или есть какая-то причина, по которой вы должны использовать Gauss-Jordan?   -  person David Heffernan    schedule 15.05.2014
comment
@DavidHeffernan, этот код был написан почти 30 лет назад. Не приходило мне в голову, что есть лучший метод.   -  person LU RD    schedule 15.05.2014
comment
@LURD Скорее всего, есть лучшее решение. Не могу сказать что это, так как не знаю проблемы.   -  person David Heffernan    schedule 15.05.2014


Ответы (2)


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

Вопрос не в точности. Скорее проблема заключается в репрезентативности. Прежде всего, давайте повторим, что числа с плавающей запятой используются для представления действительных чисел. Существует бесконечное количество действительных чисел. Конечно, то же самое можно сказать и о целых числах. Но разница здесь в том, что в определенном диапазоне есть конечное число целых чисел, но бесконечное число действительных чисел. Действительно, как изначально показал Кантор, любой конечный интервал действительных чисел содержит несчетное число действительных значений.

Итак, ясно, что мы не можем представить все действительные числа на конечной машине. Итак, какие числа мы можем изобразить? Ну, это зависит от типа данных. Типы данных с плавающей запятой Delphi используют двоичное представление. Одиночный (32-битный) и двойной (64-битный) типы соответствуют стандарту IEEE-754. Расширенный (80-битный) тип — это тип, специфичный для Intel. В двоичной системе с плавающей запятой представимое число имеет вид k2n, где k и n — целые числа. Заметьте, я не утверждаю, что все числа этой формы представимы. Это невозможно, потому что существует бесконечное количество таких чисел. Скорее я хочу сказать, что все представимые числа имеют эту форму.

Некоторые примеры представимых двоичных чисел с плавающей запятой включают: 1, 0,5, 0,25, 0,75, 1,25, 0,125, 0,375. Ваше значение 3.7 не может быть представлено как двоичное значение с плавающей запятой.

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

самый полезный веб-сайт Роба Кеннеди может показать вам наиболее близкие представимые значения к определенному числу. В случае 3.7 это:

3.7 = + 3.70000 00000 00000 00004 33680 86899 42017 73602 98112 03479 76684 57031 25
3.7 = + 3.70000 00000 00000 17763 56839 40025 04646 77810 66894 53125
3.7 = + 3.70000 00476 83715 82031 25

Они представлены в следующем порядке: удлиненные, двойные, одинарные. Другими словами, это значения ваших переменных x, d и s соответственно.

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

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

person David Heffernan    schedule 15.05.2014
comment
Отличное объяснение. Как всегда, вы делаете больше, чем отвечаете. Спасибо. - person James L.; 15.05.2014
comment
Разве k2^n не должно быть k/2^n? - person James L.; 15.05.2014
comment
@James n может быть положительным или отрицательным - person David Heffernan; 15.05.2014

Краткий ответ: 0.7 не может быть представлено точно (двоичные значения с плавающей запятой всегда представляют собой дроби со знаменателем, равным степени 2); точность типа данных, в котором вы их сохраняете (и тот, который компилятор выбирает для типа константы, с которой вы их сравниваете), может повлиять на представление этого числа и повлиять на сравнение.

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

Обязательная ссылка: Что должен знать каждый программист о вычислениях с плавающей запятой Арифметика

Еще одна ссылка, которая может оказаться полезной, – функция Delphi Math.SameValue. , что позволяет сравнивать два значения с плавающей запятой на приблизительное равенство в зависимости от конкретной допустимой дельты (разности).

person Ken White    schedule 15.05.2014
comment
Дело не в том, что число нечетное. Представление в двоичном формате с плавающей запятой означает, что оно имеет вид k/2^n. В остальном, я думаю, вы попали в точку. - person David Heffernan; 15.05.2014
comment
@David: Спасибо за редактирование. Что касается того, что вы делаете, есть ли у вас предложение лучше сформулировать его, чтобы оно было понятно нематематикам (и новым программистам) на простом языке? - person Ken White; 15.05.2014
comment
Я не уверен, что есть какой-то способ избежать хотя бы немного математики. Возможно, ближе всего было бы сказать, что двоичные значения с плавающей запятой всегда являются дробями со знаменателем, равным степени 2. - person David Heffernan; 15.05.2014
comment
@David: Это должно сработать. Я отредактировал соответственно (я не мог придумать лучшего термина). Спасибо за помощь. (Кредиты, указанные в этом комментарии и примечаниях к редакции.) - person Ken White; 15.05.2014
comment
+1, это тоже очень хороший ответ. Спасибо за ваш опыт и ссылки на статьи. - person James L.; 16.05.2014