#common-lisp
#common-lisp
Вопрос:
Для некоторого тестирования системы, в которой задействовано много плавающей арифметики, я определил диапазон отклонения плавающей арифметической ошибки, поэтому, если разница между двумя числами с плавающей запятой находится в пределах диапазона отклонения, они считаются математически равными:
;;; Floating Error Agnostic =
;;; F2 is = to F1 within the deviation range -DEV
(defparameter *flerag-devi* .0005
"Allowed deviation range, within which two floats
should be considered as mathematically equal.")
(defun flerag= (f1 f2 amp;optional (devi *flerag-devi*))
""
(<= (abs (- f1 f2)) devi))
Теперь, когда я тестирую функцию, сравнивая значение с плавающей запятой с добавленной к нему случайной дробью, ответы (по крайней мере, в отношении моих тестов функции) положительные, например:
(loop repeat 100000000
with f = 1.0
always (flerag= f ( f (random *flerag-devi*)))) ;T
Это также имеет место, когда я вычитаю случайную дробь из исходного числа с плавающей запятой, например:
(loop repeat 100000000
with f = 19.0
always (flerag= f (- f (random *flerag-devi*)))) ;T
(loop repeat 100000000
with f = 3
always (flerag= f (- f (random *flerag-devi*)))) ;T
Однако, когда я устанавливаю исходное значение float равным 1.0 (или 1):
(loop repeat 100000000
with f = 1.0
always (flerag= f (- f (random *flerag-devi*)))) ;NIL
он всегда возвращает NIL, что кажется мне более странным, поскольку он возвращает NIL сразу после вычисления (в других случаях для вычисления 100000000 раз требовалось несколько секунд). До сих пор это происходило только с f = 1.0
другими числами и никакими другими, независимо от сложения или вычитания дроби к ним. Может ли кто-нибудь воспроизвести это поведение и на своем Lisp? Любые объяснения и помощь будут оценены.
Обновить случай с f = 1.0
помощью также работает, как и с другими, когда я использую значение с двойным плавающим значением 1, т.е. 1d0 или, выполнив:
(setf *read-default-float-format* 'double-float)
Комментарии:
1. (1) вы говорите случайную «дробь», но это немного сбивает с толку (2) что касается времени выполнения, «всегда» завершит цикл раньше первого нуля, тогда как для результата T все тесты должны пройти
2. 1) Со случайной дробью я просто имел в виду (случайный флераг-деви ) 2) Я знаю об этом, мне просто любопытно, почему досрочное завершение происходит с f = 1.0 (и под `* read-default-float-format * = single), а не с другими значениями.
Ответ №1:
Добро пожаловать в арифметику с плавающей запятой: мир, где ужасные ловушки подстерегают даже осторожных, а неосторожных немедленно съедают ужасы. Посмотрите Это, копию которого вы можете получить здесь в формате PDF (эта копия может быть сомнительно законной, но у таких людей, как Oracle, также есть свободно доступные версии), и глупо, что эта статья не доступна для всех.
Что здесь происходит, так это то, что, предполагая одиночные поплавки, в какой-то момент (random 0.0005)
получается число r
(например 4.9999706E-4
), которое достаточно близко к 0.0005
этому (- 1.0 r)
0.9995
. Но (- 1.0 0.9995)
больше, чем 0.0005
: в частности, для одиночных поплавков (и, возможно, для парных тоже, но определенно для одиночных) существуют одиночные поплавки r
, такие, что
(and (< r 0.0005)
(> (- 1.0 (- 1.0 r)) 0.0005)))
Вероятно, можно систематически перечислять такие одиночные числа с плавающей запятой, но я слишком обленился.
Определение того, что значит быть «достаточно близким», также зависит от семантики того, что вы измеряете. Если вы сравниваете диаметры объектов, вы можете не захотеть, чтобы возник случай, когда оба x «достаточно близки» к y, а x>> y: 1.5E-18
находится в пределах 0.0005
1.5E-8
, но если бы вы измеряли радиус протона, вы бы вышли на десять порядков, что ненезначительная ошибка.
Для этого может подойти определение, подобное приведенному ниже (оно изменено по сравнению с предыдущим, которое было ошибочным).
(defun close-enough-p (f1 f2 amp;optional (epsilon 0.0005))
(declare (type real f1 f2 epsilon))
(let ((delta (* (abs f1) epsilon)))
(<= (- f1 delta)
f2
( f1 delta))))
Но если вы измеряете температуру в градусах Цельсия, то вы не хотите, чтобы ситуация становилась особенно суетливой вблизи нуля (если только вы не очень заботитесь об образовании льда, а в этом случае, возможно, вы так и делаете).
Однако я не специалист по плавающей запятой: я просто знаю, что все это кошмар.
В качестве несвязанного примечания: ваш loop
синтаксис не является законным. with
должно быть перед for
или другими итерационными конструкциями.
Комментарии:
1.Спасибо за ваш комментарий! Я мог бы вычислить некоторые из этих ужасных
r
s с помощью нескольких целых чисел в простом цикле. Если я не ошибаюсь, самый быстрый и безопасный способ выполнения моих тестовых задач — это преобразовать числа с плавающей запятой в виде строк в определенную десятичную точку, что-то вроде(string= (format nil "~D" 0.465188032387971243567) (format nil "~D" 0.46567714) :end1 5 :end2 5)
проверки на третью цифру после точки.2.
(close-enough-p 0.0 0.000000000000000001) ;NIL
очевидно, что since(<= 0.0 1e-20 0.0)
равно false для любого ненормализованного числа с плавающей запятой (умножается на ноль, как для f1).3. @Student: мой
close-enough-p
был нацелен на случаи, когда величина имеет значение: a не является «достаточно близким», чтобы быть, если a>> b. Я пытаюсь придумать худший способ сравнения чисел с плавающей запятой, чем путем их печати, но я не могу: если вы хотите протестировать реализацию с плавающей запятой, то вы хотите расшифровать поплавки и посмотреть на биты. Если вы хотите знать, что x = y /- дельта y, то напишите код, чтобы проверить это, и смиритесь с тем фактом, что будут нечетные граничные случаи, что нормально: числа с плавающей запятой бесполезны для точных чисел.4. На самом деле я сравниваю пиксели, для которых крошечные дробные сдвиги в течение длительного вычисления могут привести к возможным значительным изменениям в графике. Поскольку пиксели не имеют физической сущности, я думаю, что сравнение с масштабированной дельтой в моем случае не является хорошей идеей (поскольку отдельные пиксели не должны терять важность в зависимости от других критериев). В любом случае спасибо за обсуждение!
5. @Student тогда я думаю, что ваш тест на равенство в порядке, но вы не можете ожидать, что он всегда будет давать математически правильный ответ очень близко к краю интервала.