Полезность сигнализации NaN?

Недавно я немного прочитал об IEEE 754 и архитектуре x87. Я думал об использовании NaN в качестве "пропущенного значения" в каком-то числовом коде вычисления, над которым я работаю, и я надеялся, что использование сигнализации 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 для сохранения его по адресу памяти).

Вы можете думать следующее как я:

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

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

Есть ли какая-либо полезность или цель для сигнализации 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.

3 ответа

Решение

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

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

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

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

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

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

Не могли бы вы просто иметь константу 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

Другие вопросы по тегам