Подтвердить что ты не робот

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

Недавно я прочитал немного о 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.

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

Ответ 3

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