Полезность сигнализации NaN?
-
20-09-2019 - |
Вопрос
Недавно я немного прочитал об IEEE 754 и архитектуре x87.Я думал об использовании NaN в качестве «отсутствующего значения» в каком-то коде числовых вычислений, над которым я работаю, и надеялся, что использование сигнализация Нан позволила бы мне поймать исключение с плавающей запятой в тех случаях, когда я не хочу продолжать «пропущенные значения». И наоборот, я бы использовал тихий 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, чтобы сохранить его по адресу памяти).
Вы можете подумать следующее, как и я:
- Маска _EM_INVALID.
- Инициализируйте переменную, сигнализируя NaN.
- Снимите маску_EM_INVALID.
Однако шаг 2 приводит к преобразованию сигнального NaN в тихий NaN, поэтому последующее его использование будет нет вызвать исключение исключений!Так что за фигня?!
Есть ли какая-либо полезность или цель в сигнальном NaN?Я понимаю, что одним из первоначальных намерений было инициализировать с его помощью память, чтобы можно было отловить использование неинициализированного значения с плавающей запятой.
Может кто-нибудь сказать мне, если я что-то здесь упускаю?
РЕДАКТИРОВАТЬ:
Чтобы проиллюстрировать то, что я надеялся сделать, приведу пример:
Рассмотрим выполнение математических операций над вектором данных (двойниками).Для некоторых операций я хочу, чтобы вектор содержал «отсутствующее значение» (представьте, что это соответствует, например, столбцу электронной таблицы, в котором некоторые ячейки не имеют значения, но их существование важно).Для некоторых операций я делаю нет Хочу позволить вектору содержать «отсутствующее значение». Возможно, я хочу предпринять другой курс действия, если в наборе присутствует «недостающее значение» - возможно, выполняя другую операцию (таким образом, это не недействительное состояние, в котором можно быть).
Этот исходный код будет выглядеть примерно так:
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.
Решение
Насколько я понимаю, целью сигнализации NaN является инициализация структур данных, но, конечно же, время выполнения инициализация в C сопряжена с риском загрузки NaN в регистр с плавающей запятой как часть инициализации, тем самым вызывая сигнал, поскольку компилятор не знает, что это значение с плавающей запятой необходимо скопировать с использованием целочисленного регистра.
Я надеюсь, что вы могли бы инициализировать static
значение с сигнальным NaN, но даже это потребует специальной обработки со стороны компилятора, чтобы избежать его преобразования в тихий NaN.Возможно, вы могли бы использовать немного магии приведения, чтобы избежать обработки его как значения с плавающей запятой во время инициализации.
Если бы вы писали в ASM, это не было бы проблемой.но в C и особенно в C++, я думаю, вам придется разрушить систему типов, чтобы инициализировать переменную с помощью NaN.Я предлагаю использовать memcpy
.
Другие советы
Использование специальных значений (даже NULL) может сделать ваши данные намного более запутанными, а ваш код — намного более запутанным.Было бы невозможно отличить результат QNaN от «специального» значения QNaN.
Возможно, вам лучше поддерживать параллельную структуру данных для отслеживания достоверности или, возможно, хранить данные FP в другой (разреженной) структуре данных, чтобы хранить только действительные данные.
Это довольно общий совет;специальные значения очень полезны в определенных случаях (например.очень ограниченная память или производительность), но по мере увеличения контекста они могут вызвать больше трудностей, чем они того стоят.
Не могли бы вы просто иметь const uint64_t, где биты были установлены на биты сигнального nan?пока вы рассматриваете его как целочисленный тип, сигнальный 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