В чем разница между тихим NaN и сигнальным NaN?

106

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

Какой из них можно создать при программировании на C ++? Могу ли я как программист написать программу, вызывающую сигнал SNAN?

Джалал Джабери
источник

Ответы:

72

Когда операция приводит к тихому NaN, нет никаких признаков того, что что-то необычное, пока программа не проверит результат и не увидит NaN. То есть вычисления продолжаются без какого-либо сигнала от блока с плавающей запятой (FPU) или библиотеки, если с плавающей запятой реализовано программное обеспечение. Сигнальный NaN будет генерировать сигнал, обычно в виде исключения из FPU. Будет ли выброшено исключение, зависит от состояния FPU.

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

В системах POSIX / Unix исключения с плавающей запятой обычно перехватываются с помощью обработчика для SIGFPE .

Wrdieter
источник
36
В дополнение к этому: как правило, сигнализация NaN (sNaN) предназначена для отладки. Например, объекты с плавающей запятой могут быть инициализированы как sNaN. Затем, если программа не дает одному из них значения перед его использованием, возникнет исключение, когда программа использует sNaN в арифметической операции. Программа не создаст SNAN случайно; никакие нормальные операции не производят sNaN. Они создаются только специально для того, чтобы сигнализировать NaN, а не в результате каких-либо арифметических действий.
Эрик Постпищил 08
19
Напротив, NaN предназначены для более обычного программирования. Они могут быть произведены обычными операциями, когда нет числового результата (например, извлечение квадратного корня из отрицательного числа, когда результат должен быть действительным). Обычно их цель - позволить арифметике протекать нормально. Например, у вас может быть огромный массив чисел, некоторые из которых представляют особые случаи, которые невозможно обработать обычным образом. Вы можете вызвать сложную функцию для обработки этого массива, и она может работать с массивом с обычной арифметикой, игнорируя NaN. После того, как он закончится, вы должны отделить особые случаи для большей работы.
Эрик Постпищил 08
@wrdieter Спасибо, тогда только незначительная разница генерирует исключение или нет.
JalalJaberi
@EricPostpischil Спасибо за внимание ко второму вопросу.
JalalJaberi
@JalalJaberi, да, исключение - это главное отличие
wrdieter
40

Как qNaN и sNaN выглядят экспериментально?

Давайте сначала узнаем, как определить, есть ли у нас sNaN или qNaN.

Я буду использовать C ++ в этом ответе вместо C , потому что он предлагает удобный std::numeric_limits::quiet_NaNи std::numeric_limits::signaling_NaNкоторый я не мог найти в C удобно.

Однако мне не удалось найти функцию для классификации, является ли NaN sNaN или qNaN, поэтому давайте просто распечатаем необработанные байты NaN:

main.cpp

#include <cassert>
#include <cstring>
#include <cmath> // nanf, isnan
#include <iostream>
#include <limits> // std::numeric_limits

#pragma STDC FENV_ACCESS ON

void print_float(float f) {
    std::uint32_t i;
    std::memcpy(&i, &f, sizeof f);
    std::cout << std::hex << i << std::endl;
}

int main() {
    static_assert(std::numeric_limits<float>::has_quiet_NaN, "");
    static_assert(std::numeric_limits<float>::has_signaling_NaN, "");
    static_assert(std::numeric_limits<float>::has_infinity, "");

    // Generate them.
    float qnan = std::numeric_limits<float>::quiet_NaN();
    float snan = std::numeric_limits<float>::signaling_NaN();
    float inf = std::numeric_limits<float>::infinity();
    float nan0 = std::nanf("0");
    float nan1 = std::nanf("1");
    float nan2 = std::nanf("2");
    float div_0_0 = 0.0f / 0.0f;
    float sqrt_negative = std::sqrt(-1.0f);

    // Print their bytes.
    std::cout << "qnan "; print_float(qnan);
    std::cout << "snan "; print_float(snan);
    std::cout << " inf "; print_float(inf);
    std::cout << "-inf "; print_float(-inf);
    std::cout << "nan0 "; print_float(nan0);
    std::cout << "nan1 "; print_float(nan1);
    std::cout << "nan2 "; print_float(nan2);
    std::cout << " 0/0 "; print_float(div_0_0);
    std::cout << "sqrt "; print_float(sqrt_negative);

    // Assert if they are NaN or not.
    assert(std::isnan(qnan));
    assert(std::isnan(snan));
    assert(!std::isnan(inf));
    assert(!std::isnan(-inf));
    assert(std::isnan(nan0));
    assert(std::isnan(nan1));
    assert(std::isnan(nan2));
    assert(std::isnan(div_0_0));
    assert(std::isnan(sqrt_negative));
}

Скомпилируйте и запустите:

g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

вывод на моей машине x86_64:

qnan 7fc00000
snan 7fa00000
 inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
 0/0 ffc00000
sqrt ffc00000

Мы также можем выполнить программу на aarch64 в пользовательском режиме QEMU:

aarch64-linux-gnu-g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
qemu-aarch64 -L /usr/aarch64-linux-gnu/ main.out

и это дает точно такой же результат, что говорит о том, что несколько архитектур тесно реализуют IEEE 754.

На этом этапе, если вы не знакомы со структурой чисел с плавающей запятой IEEE 754, взгляните на: Что такое субнормальное число с плавающей запятой?

В двоичном формате некоторые из приведенных выше значений:

     31
     |
     | 30    23 22                    0
     | |      | |                     |
-----+-+------+-+---------------------+
qnan 0 11111111 10000000000000000000000
snan 0 11111111 01000000000000000000000
 inf 0 11111111 00000000000000000000000
-inf 1 11111111 00000000000000000000000
-----+-+------+-+---------------------+
     | |      | |                     |
     | +------+ +---------------------+
     |    |               |
     |    v               v
     | exponent        fraction
     |
     v
     sign

Из этого эксперимента мы видим, что:

  • qNaN и sNaN, похоже, различаются только битом 22: 1 означает тихо, а 0 означает сигнализацию

  • бесконечности также очень похожи с экспонентой == 0xFF, но имеют дробь == 0.

    По этой причине NaN должны установить бит 21 в 1, иначе было бы невозможно отличить sNaN от положительной бесконечности!

  • nanf() производит несколько разных NaN, поэтому должно быть несколько возможных кодировок:

    7fc00000
    7fc00001
    7fc00002
    

    Так как nan0это то же самое std::numeric_limits<float>::quiet_NaN(), мы делаем вывод, что все они разные тихие NaN.

    В C11 N1570 проект стандарта подтверждает , что nanf()порождает тихими пренебрежимо малых, потому что nanfвперед к strtodи 7.22.1.3 «The strtod, strtof и strtold функций» , говорит:

    Последовательность символов NAN или NAN (n-char-sequence opt) интерпретируется как тихий NaN, если он поддерживается в возвращаемом типе, иначе как часть субъектной последовательности, которая не имеет ожидаемой формы; значение последовательности n-символов определяется реализацией. 293)

Смотрите также:

Как qNaN и sNaN выглядят в руководствах?

IEEE 754 2008 рекомендует (обязательное или необязательное TODO?):

  • что-либо с экспонентой == 0xFF и дробью! = 0 является NaN
  • и что бит самой высокой дроби отличает qNaN от sNaN

но, похоже, не сказано, какой бит предпочтительнее для отличия бесконечности от NaN.

6.2.1 «Кодировки NaN в двоичных форматах» говорит:

В этом подпункте дополнительно определяются кодировки NaN как битовых строк, если они являются результатами операций. При кодировании все NaN имеют знаковый бит и набор битов, необходимые для идентификации кодирования как NaN и определяющего его тип (sNaN против qNaN). Остальные биты, которые находятся в конечном поле значения, кодируют полезную нагрузку, которая может быть диагностической информацией (см. Выше). 34

Все двоичные битовые строки NaN имеют все биты поля смещенной экспоненты E, равные 1 (см. 3.4). Тихая битовая строка NaN должна быть закодирована таким образом, чтобы первый бит (d1) конечного поля мантиссы T был равен 1. Сигнальная битовая строка NaN должна быть закодирована так, чтобы первый бит конечного поля значимости был равен 0. Если конечное значение поля мантиссы равно 0, какой-либо другой бит конечного значения значащего поля должен быть ненулевым, чтобы отличить NaN от бесконечности. В только что описанном предпочтительном кодировании сигнализация NaN должна быть прекращена путем установки d1 в 1, оставляя оставшиеся биты T неизменными. Для двоичных форматов полезная нагрузка кодируется в p − 2 младших значащих битах конечного поля значимости.

Руководство разработчика программного обеспечения для архитектур Intel 64 и IA-32 - Том 1 Базовая архитектура - 253665-056RU Сентябрь 2015 4.8.3.4 «NaNs» подтверждает, что x86 следует IEEE 754, различая NaN и sNaN по старшему дробному разряду:

Архитектура IA-32 определяет два класса NaN: тихие NaN (QNaN) и сигнальные NaN (SNaN). QNaN - это NaN с установленным битом старшей дроби, SNaN - это NaN с очищенным битом старшей дроби.

а также Справочное руководство по архитектуре ARM - ARMv8, для профиля архитектуры ARMv8-A - DDI 0487C.a A1.4.3 «Формат с плавающей запятой одинарной точности»:

fraction != 0: Значение - NaN, либо тихое NaN, либо сигнальное NaN. Два типа NaN различаются битом старшей дроби, битом [22]:

  • bit[22] == 0: NaN - это сигнальный NaN. Знаковый бит может принимать любое значение, а оставшиеся дробные биты могут принимать любое значение, кроме всех нулей.
  • bit[22] == 1: NaN - это тихий NaN. Знаковый бит и оставшиеся дробные биты могут принимать любое значение.

Как генерируются qNanS и sNaN?

Одно из основных различий между qNaN и sNaN заключается в следующем:

  • qNaN генерируется регулярными встроенными (программными или аппаратными) арифметическими операциями со странными значениями.
  • sNaN никогда не создается встроенными операциями, его могут явно добавить только программисты, например, с помощью std::numeric_limits::signaling_NaN

Я не смог найти четких цитат из IEEE 754 или C11 для этого, но я также не могу найти встроенную операцию, которая генерирует sNaN ;-)

Однако в руководстве Intel этот принцип четко изложен в пункте 4.8.3.4 «NaN»:

SNaN обычно используются для перехвата или вызова обработчика исключений. Они должны быть вставлены программно; то есть процессор никогда не генерирует SNaN в результате операции с плавающей запятой.

Это видно из нашего примера, где оба:

float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);

производят точно такие же биты, как std::numeric_limits<float>::quiet_NaN().

Обе эти операции компилируются в одну инструкцию сборки x86, которая генерирует qNaN непосредственно в оборудовании (TODO подтверждается GDB).

Что по-разному делают сети qNaN и SNAN?

Теперь, когда мы знаем, как выглядят qNaN и sNaN и как ими манипулировать, мы, наконец, готовы попытаться заставить sNaN делать свое дело и взорвать некоторые программы!

Итак, без лишних слов:

blow_up.cpp

#include <cassert>
#include <cfenv>
#include <cmath> // isnan
#include <iostream>
#include <limits> // std::numeric_limits
#include <unistd.h>

#pragma STDC FENV_ACCESS ON

int main() {
    float snan = std::numeric_limits<float>::signaling_NaN();
    float qnan = std::numeric_limits<float>::quiet_NaN();
    float f;

    // No exceptions.
    assert(std::fetestexcept(FE_ALL_EXCEPT) == 0);

    // Still no exceptions because qNaN.
    f = qnan + 1.0f;
    assert(std::isnan(f));
    if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
        std::cout << "FE_ALL_EXCEPT qnan + 1.0f" << std::endl;

    // Now we can get an exception because sNaN, but signals are disabled.
    f = snan + 1.0f;
    assert(std::isnan(f));
    if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
        std::cout << "FE_ALL_EXCEPT snan + 1.0f" << std::endl;
    feclearexcept(FE_ALL_EXCEPT);

    // And now we enable signals and blow up with SIGFPE! >:-)
    feenableexcept(FE_INVALID);
    f = qnan + 1.0f;
    std::cout << "feenableexcept qnan + 1.0f" << std::endl;
    f = snan + 1.0f;
    std::cout << "feenableexcept snan + 1.0f" << std::endl;
}

Скомпилируйте, запустите и получите статус выхода:

g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?

Выход:

FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136

Обратите внимание, что такое поведение происходит только -O0в GCC 8.2: с-O3 , GCC предварительно вычисляет и оптимизирует все наши операции sNaN! Я не уверен, есть ли стандартный способ предотвратить это.

Итак, из этого примера мы делаем вывод, что:

  • snan + 1.0вызывает FE_INVALID, но qnan + 1.0не

  • Linux генерирует сигнал, только если он включен с помощью feenableexept.

    Это расширение glibc, я не нашел способа сделать это ни в одном стандарте.

Когда сигнал возникает, это связано с тем, что само оборудование ЦП вызывает исключение, которое ядро ​​Linux обработало и сообщило приложению через сигнал.

Результатом является то , что отпечатки Баш Floating point exception (core dumped), а статус выхода 136, который соответствует сигналу 136 - 128 == 8, который в соответствии с:

man 7 signal

есть SIGFPE.

Обратите внимание, что SIGFPEэто тот же сигнал, который мы получаем, если пытаемся разделить целое число на 0:

int main() {
    int i = 1 / 0;
}

хотя для целых чисел:

  • деление чего-либо на ноль вызывает сигнал, так как нет представления бесконечности в целых числах
  • сигнал, что это происходит по умолчанию, без необходимости feenableexcept

Как справиться с SIGFPE?

Если вы просто создаете обработчик, который обычно возвращается, это приведет к бесконечному циклу, потому что после возврата обработчика деление происходит снова! Это можно проверить с помощью GDB.

Единственный способ - использовать setjmpи longjmpперейти в другое место, как показано на: C обработать сигнал SIGFPE и продолжить выполнение

Каковы реальные применения сетей SNAN?

Честно говоря, я до сих пор не понял суперполезного варианта использования sNaN. Его спросили на: Полезность сигнализации NaN?

sNaN кажутся особенно бесполезными, потому что мы можем обнаружить начальные недопустимые операции ( 0.0f/0.0f), которые генерируют qNaN с помощью feenableexcept: похоже, что snanпросто вызывает ошибки для большего количества операций, которые qnanне возникают , например, (qnan + 1.0f ).

Например:

main.c

#define _GNU_SOURCE
#include <fenv.h>
#include <stdio.h>

int main(int argc, char **argv) {
    (void)argv;
    float f0 = 0.0;

    if (argc == 1) {
        feenableexcept(FE_INVALID);
    }
    float f1 = 0.0 / f0;
    printf("f1 %f\n", f1);

    feenableexcept(FE_INVALID);
    float f2 = f1 + 1.0;
    printf("f2 %f\n", f2);
}

компилировать:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm

тогда:

./main.out

дает:

Floating point exception (core dumped)

а также:

./main.out  1

дает:

f1 -nan
f2 -nan

См. Также: Как отследить NaN в C ++

Что такое сигнальные флаги и как ими манипулировать?

Все реализовано в аппаратном обеспечении ЦП.

Флаги находятся в каком-то регистре, как и бит, который говорит, следует ли поднять исключение / сигнал.

Эти регистры доступны из пользовательского пространства из большинства арок.

Эта часть кода glibc 2.29 на самом деле очень проста для понимания!

Например, fetestexceptдля x86_86 это реализовано в sysdeps / x86_64 / fpu / ftestexcept.c :

#include <fenv.h>

int
fetestexcept (int excepts)
{
  int temp;
  unsigned int mxscr;

  /* Get current exceptions.  */
  __asm__ ("fnstsw %0\n"
       "stmxcsr %1" : "=m" (*&temp), "=m" (*&mxscr));

  return (temp | mxscr) & excepts & FE_ALL_EXCEPT;
}
libm_hidden_def (fetestexcept)

так что сразу видно, что инструкция по использованию stmxcsrТаким означает «Store MXCSR Register State».

И feenableexceptреализовано в sysdeps / x86_64 / fpu / feenablxcpt.c :

#include <fenv.h>

int
feenableexcept (int excepts)
{
  unsigned short int new_exc, old_exc;
  unsigned int new;

  excepts &= FE_ALL_EXCEPT;

  /* Get the current control word of the x87 FPU.  */
  __asm__ ("fstcw %0" : "=m" (*&new_exc));

  old_exc = (~new_exc) & FE_ALL_EXCEPT;

  new_exc &= ~excepts;
  __asm__ ("fldcw %0" : : "m" (*&new_exc));

  /* And now the same for the SSE MXCSR register.  */
  __asm__ ("stmxcsr %0" : "=m" (*&new));

  /* The SSE exception masks are shifted by 7 bits.  */
  new &= ~(excepts << 7);
  __asm__ ("ldmxcsr %0" : : "m" (*&new));

  return old_exc;
}

Что стандарт C говорит о qNaN и sNaN?

В проекте стандарта C11 N1570 прямо говорится, что стандарт не делает различий между ними в F.2.1 «Бесконечности, нули со знаком и NaN»:

1 Эта спецификация не определяет поведение сигнализации NaN. Обычно он использует термин NaN для обозначения тихих NaN. Макросы NAN и INFINITY, а также функции nan в <math.h>предоставляют обозначения для значений NaN и бесконечности согласно IEC 60559.

Протестировано в Ubuntu 18.10, GCC 8.2. Апстримы GitHub:

Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
en.wikipedia.org/wiki/IEEE_754#Interchange_formats указывает, что IEEE-754 просто предполагает, что 0 для сигнализации NaN - это хороший выбор реализации, позволяющий успокоить NaN, не рискуя сделать его бесконечным (значимое = 0). Видимо, это не стандартизовано, хотя x86 это делает. (И тот факт, что именно старший бит мантиссы определяет qNaN по сравнению с sNaN , стандартизирован). en.wikipedia.org/wiki/Single-precision_floating-point_format говорит, что x86 и ARM одинаковы, но PA-RISC сделал противоположный выбор.
Питер Кордес
@PeterCordes да, я не уверен, что «следует» == «должен» или «предпочтительнее» в IEEE 754 20at «Сигнальная битовая строка NaN должна быть закодирована с первым битом конечного поля значимости, равным 0».
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
re: но, похоже, не указывается, какой бит следует использовать, чтобы отличать бесконечность от NaN. Вы написали, что, как и ожидали, должен быть какой-то конкретный бит, который стандарт рекомендует устанавливать, чтобы отличать sNaN от бесконечности. IDK, почему вы ожидаете, что будет такой бит; любой ненулевой выбор подойдет. Просто выберите что-то, что позже определит, откуда пришел sNaN. IDK, звучит как странная формулировка, и мое первое впечатление при чтении было то, что вы говорили, что веб-страница не описывает то, что отличает inf от NaN в кодировке (нулевое значение).
Питер Кордес
До 2008 года IEEE 754 указывал, какой бит является сигнальным / тихим (бит 22), но не указывал, какое значение что определяет. Большинство процессоров сконцентрировались на 1 = тихо, так что это стало частью стандарта в редакции 2008 года. Он говорит «должен», а не «должен», чтобы избежать создания несовместимых с предыдущими реализациями того же выбора. В общем, «должен» стандартным образом означает «должен, если у вас нет очень веских (и желательно хорошо задокументированных) причин несоблюдения».
Джон Коуэн