Каково обоснование для всех сравнений, возвращающих ложь для значений NaN IEEE754?
Почему сравнения значений NaN ведут себя иначе, чем все другие значения? То есть все сравнения с операторами ==, <=, >=, <,>, где одним или обоими значениями является NaN, возвращают false, что противоречит поведению всех других значений.
Я предполагаю, что это каким-то образом упрощает численные вычисления, но я не смог найти явно заявленную причину, даже в " Лекционных заметках о состоянии IEEE 754 " Кахана, в которых подробно обсуждаются другие проектные решения.
Такое отклоняющееся поведение вызывает проблемы при выполнении простой обработки данных. Например, при сортировке списка записей по некоторому вещественному полю в программе на Си мне нужно написать дополнительный код для обработки NaN в качестве максимального элемента, иначе алгоритм сортировки может запутаться.
Изменить: ответы до сих пор все утверждают, что бессмысленно сравнивать NaNs.
Я согласен, но это не значит, что правильный ответ ложный, скорее это будет не-булев (NaB), которого, к счастью, не существует.
Так что выбор возврата true или false для сравнений, на мой взгляд, произвольный, и для общей обработки данных было бы выгодно, если бы он подчинялся обычным законам (рефлексивность ==, трихотомия <, ==,>), чтобы структуры данных которые полагаются на эти законы, становятся запутанными.
Поэтому я спрашиваю о каком-то конкретном преимуществе нарушения этих законов, а не только философских рассуждениях.
Редактировать 2: Я думаю, что теперь я понимаю, почему сделать максимальный NaN было бы плохой идеей, это испортило бы вычисление верхних пределов.
NaN! = NaN может быть желательно, чтобы избежать обнаружения сходимости в цикле, таких как
while (x != oldX) {
oldX = x;
x = better_approximation(x);
}
который, однако, лучше написать, сравнивая абсолютную разницу с небольшим пределом. Так что ИМХО это относительно слабый аргумент для нарушения рефлексивности в NaN.
14 ответов
Я был членом комитета IEEE-754, я постараюсь помочь немного прояснить ситуацию.
Во-первых, числа с плавающей точкой не являются действительными числами, а арифметика с плавающей точкой не удовлетворяет аксиомам реальной арифметики. Трихотомия - не единственное свойство настоящей арифметики, которое не распространяется на поплавки и даже не является самым важным. Например:
- Дополнение не ассоциативно.
- Распределительный закон не имеет места.
- Есть числа с плавающей точкой без инверсий.
Я мог бы продолжить. Невозможно указать арифметический тип фиксированного размера, который удовлетворяет всем свойствам реальной арифметики, которые мы знаем и любим. Комитет 754 должен решить согнуть или сломать некоторые из них. Это руководствуется некоторыми довольно простыми принципами:
- Когда мы можем, мы сопоставляем поведение реальной арифметики.
- Когда мы не можем, мы стараемся сделать нарушения максимально предсказуемыми и легко диагностируемыми.
Что касается вашего комментария "это не значит, что правильный ответ ложный", это неправильно. Предикат (y < x)
спрашивает, y
меньше чем x
, Если y
равен NaN, то он не меньше, чем любое значение с плавающей точкой x
поэтому ответ обязательно ложный.
Я упоминал, что трихотомия не выполняется для значений с плавающей точкой. Тем не менее, есть аналогичное свойство, которое имеет место. Пункт 5.11, пункт 2 стандарта 754-2008:
Возможны четыре взаимоисключающих отношения: меньше, равно, больше, и неупорядочено. Последний случай возникает, когда хотя бы один операнд является NaN. Каждый NaN должен сравнивать неупорядоченное со всем, включая себя.
Что касается написания дополнительного кода для обработки NaN, то обычно возможно (хотя и не всегда легко) структурировать ваш код таким образом, чтобы NaN проходили должным образом, но это не всегда так. В противном случае может потребоваться дополнительный код, но это небольшая цена за удобство, которое алгебраическое замыкание принесло арифметике с плавающей точкой.
Приложение: Многие комментаторы утверждают, что было бы более полезно сохранить рефлексивность равенства и трихотомии на том основании, что принятие NaN!= NaN, похоже, не сохраняет какой-либо знакомой аксиомы. Признаюсь, что сочувствую этой точке зрения, поэтому я подумал, что вернусь к этому ответу и предоставлю немного больше контекста.
Насколько я понимаю из разговора с Каханом, NaN!= NaN возникла из двух прагматических соображений:
Тот
x == y
должно быть эквивалентноx - y == 0
всякий раз, когда это возможно (помимо теоремы о реальной арифметике, это делает аппаратную реализацию сравнения более компактной, что было крайне важно во время разработки стандарта - однако следует отметить, что это нарушается при x = y = бесконечности, так что это не очень хорошая причина, она могла бы(x - y == 0) or (x and y are both NaN)
).Что еще более важно, не было
isnan( )
предикат в то время, когда NaN был формализован в арифметике 8087; было необходимо предоставить программистам удобные и эффективные средства обнаружения значений NaN, которые не зависели бы от языков программирования, обеспечивающих что-то вродеisnan( )
что может занять много лет. Я процитирую собственное письмо Кахана на эту тему:
Если бы не было способа избавиться от NaN, они были бы такими же бесполезными, как Indefinites на CRAY; как только они встретятся, вычисление лучше остановить, чем продолжать на неопределенное время до неопределенного завершения. Вот почему некоторые операции с NaN должны давать результаты, отличные от NaN. Какие операции? … Исключение составляют предикаты C "x == x" и "x!= X", которые соответственно равны 1 и 0 для каждого бесконечного или конечного числа x, но обращаются, если x не является числом ( NaN); они обеспечивают единственное простое исключительное различие между NaN и числами в языках, в которых отсутствует слово для NaN и предикат IsNaN(x).
Обратите внимание, что это также логика, которая исключает возвращение чего-то вроде "Not-A-Boolean". Может быть, этот прагматизм был неуместен, и стандарт должен был требовать isnan( )
, но это сделало бы практически невозможным эффективное и удобное использование NaN в течение нескольких лет, пока мир ждал принятия языка программирования. Я не уверен, что это был бы разумный компромисс.
Чтобы быть тупым: результат NaN == NaN не изменится сейчас. Лучше научиться жить с этим, чем жаловаться в интернете. Если вы хотите утверждать, что отношение порядка, подходящее для контейнеров, также должно существовать, я бы рекомендовал рекомендовать вашему любимому языку программирования реализовать totalOrder
предикат стандартизирован в IEEE-754 (2008). Тот факт, что это еще не говорит об обоснованности озабоченности Кахана, которая мотивировала текущее положение дел.
NaN можно рассматривать как неопределенное состояние / число. похож на концепцию 0/0 неопределенности или sqrt(-3) (в реальной системе счисления, где живет с плавающей запятой).
NaN используется как своего рода заполнитель для этого неопределенного состояния. Математически говоря, undefined не равен undefined. Вы также не можете сказать, что неопределенное значение больше или меньше другого неопределенного значения. Поэтому все сравнения возвращают false.
Такое поведение также полезно в тех случаях, когда вы сравниваете sqrt (-3) с sqrt(-2). Они оба вернут NaN, но они не эквивалентны, даже если они возвращают одно и то же значение. Поэтому наличие равенства, всегда возвращающего ложь при работе с NaN, является желаемым поведением.
Приведу еще одну аналогию. Если я передам вам две коробки и скажу, что ни в одной из них нет яблока, не могли бы вы сказать, что в коробках содержится одно и то же?
NaN не содержит информации о том, что является чем-то, только что это не так. Поэтому эти элементы никогда нельзя назвать равными.
Из статьи Википедии о NaN, следующие методы могут вызвать NaN:
- Все математические операции> с NaN в качестве хотя бы одного операнда
- Деления 0/0, ∞/∞, ∞/-∞, -∞/∞ и -∞/-∞
- Умножения 0×∞ и 0×-∞
- Сложения ∞ + (-∞), (-∞) + ∞ и эквивалентные вычитания.
- Применение функции к аргументам вне ее области, включая получение квадратного корня из отрицательного числа, получение логарифма отрицательного числа, получение тангенса нечетного кратного 90 градусам (или π/2 радиан) или обратного синуса или косинус числа, которое меньше -1 или больше +1.
Поскольку нет способа узнать, какая из этих операций создала NaN, нет никакого способа сравнить их, что имеет смысл.
Я не знаю обоснования дизайна, но вот выдержка из стандарта IEEE 754-1985:
"Должна быть возможность сравнивать числа с плавающей запятой во всех поддерживаемых форматах, даже если форматы операндов различаются. Сравнения точны и никогда не переполняются и не переполняются. Возможны четыре взаимоисключающих отношения: меньше, равно, больше, и неупорядочено. Последний случай возникает, когда хотя бы один операнд является NaN. Каждый NaN должен сравнивать неупорядоченный со всем, включая себя ".
Это выглядит только странно, потому что большинство сред программирования, которые допускают использование NaN, также не допускают 3-значную логику. Если вы добавите 3-значную логику в смесь, она станет последовательной:
- (2.7 == 2.7) = верно
- (2.7 == 2.6) = неверно
- (2.7 == NaN) = неизвестно
- (NaN == NaN) = неизвестно
Даже.NET не предоставляет bool? operator==(double v1, double v2)
оператор, так что вы все еще застряли с глупым (NaN == NaN) = false
результат.
MeToo пришел сюда, чтобы понять аргументацию, почему равно false.
Прочитав (почти) все, я все еще недоумевал, почемуa == NaN
не может заменить такую функцию, какisNaN()
, потому что это кажется таким очевидным.
Но все не так просто.
Никто еще не упомянул векторную геометрию. Но многие вычисления происходят во 2-м или 3-м измерении, то есть в векторном пространстве.
Немного подумав об этом, я сразу понял, почему хорошо не сравнивать себя с собой. Следование, надеюсь, достаточно легко понять и для других.
Векторы
Потерпите меня, это займет некоторое время, прежде чем появится.
Сначала позвольте мне немного объяснить для людей, которые не очень хорошо разбираются в математике.
В векторной геометрии мы обычно используем что-то вроде комплексных чисел.
Комплексное число состоит из двух чисел с плавающей запятойa + bi
(гдеi
обозначает мнимое значение сi * i == -1
), которые позволяют нам обращаться ко всем точкам на двумерной плоскости. С плавающей запятой мы не можем выразить каждое значение, поэтому нам нужно немного приблизиться. Таким образом, если мы округлим значения до некоторого значения, которое мы можем выразить, мы все равно можем попытаться создать численно устойчивые алгоритмы, которые дают нам хорошее приближение к тому, что мы хотим заархивировать.
Введите бесконечность
Здесь пока нет. Пожалуйста, будьте терпеливы. Я перейду к делу позже внизу.
Если мы хотим указать какую-то очень далекую точку, мы можем оставить диапазон чисел, которые мы можем выразить, что приведет к бесконечности. В IEEE float у нас, к счастью, есть+inf
(я пишу это как ) или для этого (пишется как-inf
).
Это хорошо:
a + inf i
имеет смысл, верно? Это вектор в некоторую точку по оси X в месте и по оси Y в месте «положительная бесконечность». Но подождите немного, мы говорим о векторах здесь!
Векторы имеют начало и точку, на которую они указывают. Нормализованные векторы — это те, которые начинаются в точке .
Теперь подумайте о векторе с началом(0,0)
что указывает на(a,inf)
.
Все еще имеет смысл? Не совсем. Если мы посмотрим поближе, то увидим, что нормализованный вектор — это тот же самый вектор! Поскольку вектор такой длинный, выводa
в бесконечности больше не видно. Или сказал иначе:
Для бесконечно длинных векторов в декартовой системе координат конечная ось может быть выражена как , потому что нам разрешено аппроксимировать (если нам не разрешено аппроксимировать, мы не можем использовать плавающую точку!).
Так что замещающий вектор все еще подходит. На самом деле любой(x,inf)
является подходящей заменой конечногоx
. Так почему бы не использовать0
от нашего начала нашего нормализованного вектора.
Следовательно, что мы здесь получаем? ну с позволениемinf
в наших векторах мы фактически получаем 8 возможных бесконечных векторов, каждый из которых повернут на 45 градусов (градусы в скобках):
(inf,0)
(0),(inf,inf)
(45),(0,inf)
(90),(-inf,inf)
(135),(-inf,0)
(180),(-inf,-inf)
(225),(0,-inf)
(270) и(inf,-inf)
(315)
Все это не доставляет никаких хлопот. На самом деле хорошо иметь возможность выражать больше, чем просто конечные векторы. Таким образом, у нас есть естественное расширение нашей модели.
Полярные координаты
Еще нет, но мы приближаемся
Выше мы использовали комплексные числа в качестве декартовых координат. Но у комплексных чисел также есть 2-й вариант, как мы можем их записать. Это полярные координаты.
Полярные координаты состоят из длины и угла, как[angle,length]
. Итак, если мы преобразуем наше комплексное число в полярные координаты, мы увидим, что можем выразить немного больше, чем просто 8 углов в[angle,inf]
.
Следовательно, если вы хотите создать математическую модель, которая допускает бесконечно длинные векторы в каком-то многомерном пространстве, вы определенно хотите использовать полярные координаты в своих вычислениях настолько часто, насколько это возможно.
Все, что вам нужно для этого сделать, это преобразовать декартовы координаты в полярные и наоборот.
Как это сделать, оставляем читателю в качестве упражнения .
Входить
Итак, что у нас есть?
- У нас есть математическая модель, которая вычисляет с полярными координатами.
- И у нас есть какое-то устройство вывода, которое, наверное, использует декартовы координаты.
Что мы сейчас хотим сделать, так это иметь возможность конвертировать между этими двумя. Что нам нужно для этого?
Конечно, нам нужна плавающая точка!
И поскольку нам, возможно, нужно вычислить несколько терамиллиардов координат (возможно, мы представляем прогноз погоды или имеем некоторые данные о столкновениях с большого адронного коллайдера), мы не хотим включать медленную и подверженную ошибкам обработку ошибок (WTF? Обработка ошибок, подверженная ошибкам). Спорим!) во всех этих сложных математических (надеюсь, численно стабильных) шагах.
Как мы тогда распространяем ошибки?
Ну, как сказал IEEE: мы используем
NaN
для распространения ошибок
Итак, что мы имеем здесь?
- Некоторый расчет в полярном координатном пространстве
- Некоторое преобразование в декартово пространство
- NaN как спасение, если что-то пошло не так
И это потом приводит к..
.. почему должно быть ложным
Чтобы объяснить это, давайте сведем этот сложный материал, прежде всего, к простому результату двух векторов в декартовых координатах:
-
(a,b)
и(c,d)
И мы хотим сравнить эти два. Вот как выглядит это сравнение:
-
a == c && b == d
Пока все правильно?
Да. Но только до тех пор, пока мы не наблюдаем следующие два полярных вектора, которые могут быть источником наших двух декартовых векторов:
-
[NaN,inf]
и[0,NaN]
Конечно, эти два вектора не равны в пространстве полярных координат . Но после преобразования в декартово пространство оба получаются как:
- и
(NaN,NaN)
Ну а вдруг они равные сравнивать должны?
Конечно нет!
Благодаря определению IEEE, чтоNaN == NaN
должен вернутьсяfalse
, наше очень примитивное векторное сравнение все же дает нам ожидаемый результат!
И я думаю, что это именно та мотивация, по которой IEEE определил его таким, какой он есть.
Теперь нам с этим бардаком жить. Но действительно ли это беспорядок? Я не определился. Но, по крайней мере, теперь я могу понять (вероятную) аргументацию.
Надеюсь, я ничего не пропустил.
Несколько последних слов
Примитивный способ сравнения вещей обычно не совсем уместен, когда речь идет о числах с плавающей запятой.
В плавающей запятой вы обычно не используете , вы скорее используете что-то вродеabs(a-b) < eps
сeps
быть очень малой величиной. Это потому, что уже что-то вроде1/3 + 1/3 * 2.0 == 1.0
может быть неверным, в зависимости от того, какое оборудование вы используете.
1/3 + 1/3 * 2.0 == 1/3 + 1/3 + 1/3
должно быть верно на всех разумных аппаратных средствах. Так что даже==
может быть использован. Только осторожно. Но не исключено.
Однако это не делает вышеприведенные рассуждения недействительными. Потому что вышеизложенное не является математическим доказательством того, что IEEE прав. Это всего лишь пример, который должен позволить понять источник рассуждений и почему, вероятно, лучше определить его таким, какой он есть.
Даже то, что это PITA для всех программистов вроде меня.
Я предполагаю, что NaN (не число) означает именно это: это не число, и поэтому сравнивать его не имеет смысла.
Это немного похоже на арифметику в SQL с null
операнды: все они приводят к null
,
Сравнения для чисел с плавающей запятой сравнивают числовые значения. Таким образом, они не могут быть использованы для нечисловых значений. Поэтому NaN нельзя сравнивать в числовом смысле.
Упрощенный ответ заключается в том, что NaN не имеет числового значения, поэтому нет ничего, что можно было бы сравнить с чем-либо еще.
Вы можете рассмотреть возможность проверки и замены ваших NaN на +INF, если вы хотите, чтобы они действовали как +INF.
Хотя я согласен с тем, что сравнение NaN с любым действительным числом должно быть неупорядоченным, я думаю, что есть только причина для сравнения NaN с самим собой. Как, например, обнаружить разницу между сигнальными NaN и тихими NaN? Если мы рассматриваем сигналы как набор булевых значений (то есть битовый вектор), можно было бы спросить, являются ли битовые векторы одинаковыми или разными, и упорядочить наборы соответствующим образом. Например, при декодировании показателя максимального смещения, если бы значени и были сдвинуты влево для выравнивания старшего значащего бита значащего и по старшему биту двоичного формата, отрицательным значением будет тихий NaN, а любое положительное значение будет быть сигнальным NaN. Ноль, конечно, зарезервирован для бесконечности, и сравнение будет неупорядоченным. Выравнивание MSB позволит прямое сравнение сигналов даже из разных двоичных форматов. Поэтому два NaN с одинаковым набором сигналов будут эквивалентны и придают значение равенству.
NaN - это неявный новый экземпляр (особого вида ошибки времени выполнения). Это означает NaN !== NaN
по той же причине, что new Error !== new Error
;
И имейте в виду, что такая неявность также видна за пределами ошибок, например, в контексте регулярных выражений это означает /a/ !== /a/
который является просто синтаксисом сахара для new RegExp('a') !== new RegExp('a')
Потому что математика - это та область, где числа "просто существуют". При вычислениях вы должны инициализировать эти числа и поддерживать их состояние в соответствии с вашими потребностями. В те давние времена инициализация памяти работала так, как вы никогда не могли положиться. Вы никогда не могли позволить себе думать об этом "о, это будет все время инициализироваться с 0xCD, мой алгоритм не сломается".
Таким образом, вам нужен надлежащий не смешивающий растворитель, который достаточно липкий, чтобы не позволить вашему алгоритму быть втянутым и сломанным. Хорошие алгоритмы, включающие числа, в основном будут работать с отношениями, и те отношения if() будут опущены.
Это просто смазка, которую вы можете поместить в новую переменную при создании, вместо программирования случайного ада из памяти компьютера. И ваш алгоритм, каким бы он ни был, не сломается.
Затем, когда вы все еще внезапно обнаружите, что ваш алгоритм генерирует NaN, можно очистить его, просматривая каждую ветвь по одному. Опять же, "всегда ложное" правило очень помогает в этом.
Для меня самый простой способ объяснить это:
У меня есть что-то, и если это не яблоко, то это апельсин?
Вы не можете сравнить NaN с чем-то другим (даже с самим собой), потому что у него нет значения. Также это может быть любое значение (кроме числа).
У меня есть что-то, и если оно не равно числу, то это строка?
Очень короткий ответ:
Потому что следующее:
nan / nan = 1
НЕ должен держать. Иначе inf/inf
будет 1.
(Следовательно nan
не может быть равным nan
, Что касается >
или же <
, если nan
будет уважать любое отношение порядка в множестве, удовлетворяющем свойству Архимеда, мы бы снова nan / nan = 1
на пределе).