Недавно я прочитал немного о IEEE 754 и архитектуре x87. Я подумывал использовать NaN как "недостающее значение" в некотором числовом коде вычисления, над которым я работаю, и я надеялся, что использование сигнальной NaN позволит мне поймать исключение с плавающей точкой в тех случаях, когда я не хочу выполните "отсутствующие значения". И наоборот, я бы использовал тихое NaN, чтобы позволить "пропущенное значение" распространяться через вычисление. Однако сигнальные NaN не работают, поскольку я думал, что они будут основываться на (очень ограниченной) документации, которая существует на них.
Вот краткое изложение того, что я знаю (все это с использованием x87 и VС++):
- _EM_INVALID (исключающее исключение IEEE) управляет поведением x87 при встрече с NaNs
- Если _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.