Вопрос

Недавно я немного прочитал об 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, чтобы сохранить его по адресу памяти).

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

  1. Маска _EM_INVALID.
  2. Инициализируйте переменную, сигнализируя NaN.
  3. Снимите маску_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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top