Полезность сигнализации NaN? - PullRequest
49 голосов
/ 11 февраля 2010

Я недавно немного прочитал о IEEE 754 и архитектуре x87. Я думал об использовании NaN в качестве «пропущенного значения» в некотором числовом коде вычисления, над которым я работаю, и я надеялся, что использование signaling NaN позволит мне перехватить исключение с плавающей запятой в тех случаях, Я не хочу переходить к «пропущенным значениям». И наоборот, я бы использовал quiet NaN, чтобы "пропущенное значение" распространялось через вычисления. Однако сигнальные NaN не работают так, как я думал, они будут работать на основе (очень ограниченной) документации, которая существует на них.

Вот краткое изложение того, что я знаю (все это с использованием x87 и VC ++):

  • _EM_INVALID (недопустимое исключение IEEE) управляет поведением x87 при обнаружении NaN
  • Если _EM_INVALID замаскирован (исключение отключено), исключение не создается, и операции могут возвращать тихий NaN. Операция, включающая сигнализацию NaN, будет не вызывать исключение, но будет преобразована в тихий NaN.
  • Если _EM_INVALID не маскируется (исключение включено), недопустимая операция (например, sqrt (-1)) вызывает недопустимое исключение.
  • x87 никогда не генерирует сигнализацию NaN.
  • Если _EM_INVALID не маскируется, любое использование сигнального NaN (даже инициализация переменной с ним) вызывает недопустимое исключение.

Стандартная библиотека предоставляет способ доступа к значениям NaN:

std::numeric_limits<double>::signaling_NaN();

и

std::numeric_limits<double>::quiet_NaN();

Проблема в том, что я не вижу никакой пользы для сигнализации NaN. Если _EM_INVALID замаскирован, он ведет себя точно так же, как тихий NaN. Поскольку ни один NaN не сравним с любым другим NaN, логической разницы нет.

Если _EM_INVALID не замаскирован (исключение включено), то невозможно даже инициализировать переменную с сигнальным NaN: double dVal = std::numeric_limits<double>::signaling_NaN(); потому что это вызывает исключение (сигнальное значение NaN загружается в регистр x87 для сохранения его по адресу памяти).

Вы можете думать так же, как я:

  1. Маска _EM_INVALID.
  2. Инициализировать переменную с помощью сигнализации NaN.
  3. Unmask_EM_INVALID.

Однако, шаг 2 заставляет сигнальный NaN преобразовываться в тихий NaN, поэтому последующее его использование не вызывает исключения! Так что WTF?!

Есть ли какая-либо полезность или цель для сигнализации NaN? Я понимаю, что одно из первоначальных намерений состояло в том, чтобы инициализировать память таким образом, чтобы можно было использовать единичное значение с плавающей запятой.

Может кто-нибудь сказать мне, если я что-то здесь упускаю?


EDIT:

Для дальнейшей иллюстрации того, что я надеялся сделать, вот пример:

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

Этот оригинальный код будет выглядеть примерно так:

const double MISSING_VALUE = 1.3579246e123;
using std::vector;

vector<double> missingAllowed(1000000, MISSING_VALUE);
vector<double> missingNotAllowed(1000000, MISSING_VALUE);

// ... populate missingAllowed and missingNotAllowed with (user) data...

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) {
    if (*it != MISSING_VALUE) *it = sqrt(*it); // sqrt() could be any operation
}

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) {
    if (*it != MISSING_VALUE) *it = sqrt(*it);
    else *it = 0;
}

Обратите внимание, что проверка на "пропущенное значение" должна выполняться каждая итерация цикла . Хотя я понимаю, что в большинстве случаев функция sqrt (или любая другая математическая операция), вероятно, затмевает эту проверку, есть случаи, когда операция минимальна (возможно, просто дополнение), и проверка является дорогостоящей. Не говоря уже о том, что «отсутствующее значение» выводит допустимое входное значение из строя и может привести к ошибкам, если вычисление достигнет этого значения (маловероятно, хотя это может быть). Кроме того, чтобы быть технически правильными, входные данные пользователя должны быть сопоставлены с этим значением, и должен быть предпринят соответствующий порядок действий. Я считаю это решение не элегантным и неоптимальным с точки зрения производительности. Это критичный для производительности код, и у нас определенно нет такой роскоши, как параллельные структуры данных или какие-либо объекты элементов данных.

Версия NaN будет выглядеть так:

using std::vector;

vector<double> missingAllowed(1000000, std::numeric_limits<double>::quiet_NaN());
vector<double> missingNotAllowed(1000000, std::numeric_limits<double>::signaling_NaN());

// ... populate missingAllowed and missingNotAllowed with (user) data...

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) {
    *it = sqrt(*it); // if *it == QNaN then sqrt(*it) == QNaN
}

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) {
    try {
        *it = sqrt(*it);
    } catch (FPInvalidException&) { // assuming _seh_translator set up
        *it = 0;
    }
}

Теперь явная проверка устранена, и производительность должна быть улучшена. Я думаю, что все это сработало бы, если бы я мог инициализировать вектор, не касаясь регистров FPU ...

Кроме того, я хотел бы представить, чтобы любая уважающая себя реализация sqrt проверяла NaN и немедленно возвращала NaN.

Ответы [ 3 ]

9 голосов
/ 12 февраля 2010

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

Я надеюсь, что вы могли бы инициализировать значение static сигнальным NaN, но даже это потребовало бы некоторой специальной обработки компилятором, чтобы избежать его преобразования в тихий NaN. Возможно, вы могли бы использовать немного магии наложения, чтобы избежать ее обработки как значения с плавающей точкой во время инициализации.

Если бы вы писали в ASM, это не было бы проблемой. но в C и особенно в C ++, я думаю, вам придется подорвать систему типов, чтобы инициализировать переменную с помощью NaN. Я предлагаю использовать memcpy.

2 голосов
/ 17 августа 2015

Не могли бы вы просто иметь константу uint64_t, в которой биты были установлены на биты сигнальной нана? пока вы рассматриваете его как целочисленный тип, сигнальный nan не отличается от других целых чисел. Вы можете написать это где угодно через приведение указателя:

Const uint64_t sNan = 0xfff0000000000000;
Double[] myData;
...
Uint64* copier = (uint64_t*) &myData[index];
*copier=sNan | myErrorFlags;

Информация о битах для установки: https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html

2 голосов
/ 13 февраля 2010

Использование специальных значений (даже NULL) может сделать ваши данные намного грязнее, а ваш код - намного более грязным. Было бы невозможно различить результат QNaN и «специальное» значение QNaN.

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

Это довольно общий совет; специальные значения очень полезны в некоторых случаях (например, действительно ограниченная память или ограничения производительности), но по мере увеличения контекста они могут вызвать больше трудностей, чем они стоят.

...