Я недавно немного прочитал о 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 для сохранения его по адресу памяти).
Вы можете думать так же, как я:
- Маска _EM_INVALID.
- Инициализировать переменную с помощью сигнализации NaN.
- 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.