В чем разница между тихим NaN и сигнальным NaN?
Я читал о плавающей точке, и я понимаю, что NaN может быть результатом операций. но я не могу понять, что это за понятия. Какая разница?
Какой из них можно создать во время программирования на C++? Как программист, могу ли я написать программу, вызывающую sNaN?
1 ответ
Когда операция приводит к тихому NaN, нет никаких признаков того, что что-то необычное, пока программа не проверит результат и не увидит NaN. Таким образом, вычисление продолжается без какого-либо сигнала от модуля с плавающей запятой (FPU) или библиотеки, если с плавающей запятой реализована в программном обеспечении. Сигнализация NaN будет генерировать сигнал, обычно в форме исключения из FPU. Будет ли выброшено исключение, зависит от состояния FPU.
C++ 11 добавляет несколько языковых элементов управления средой с плавающей запятой и предоставляет стандартизированные способы создания и тестирования для NaN. Однако то, реализованы ли элементы управления, не очень хорошо стандартизировано, и исключения с плавающей точкой обычно не отлавливаются так же, как стандартные исключения C++.
В системах POSIX/Unix исключения с плавающей запятой обычно перехватываются с помощью обработчика для SIGFPE.
Как qNaNs и sNaNs выглядят экспериментально?
Давайте сначала узнаем, как определить, есть ли у нас 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()
Мы заключаем, что они все разные тихие NaNs.Проект стандарта C11 N1570 подтверждает, что
nanf()
генерирует тихие NaNs, потому чтоnanf
вперед кstrtod
и 7.22.1.3 "Функции strtod, strtof и strtold" говорят:Последовательность символов NAN или NAN (n-char-sequence opt) интерпретируется как тихий NaN, если поддерживается в типе возврата, иначе как часть последовательности субъекта, которая не имеет ожидаемой формы; смысл последовательности n-char определяется реализацией. 293)
Смотрите также:
Как выглядят qNaNs и sNaNs в руководствах?
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?
Одно из основных различий между qNaNs и sNaNs заключается в том, что:
- qNaN генерируется обычными встроенными (программными или аппаратными) арифметическими операциями со странными значениями
- sNaN никогда не генерируется встроенными операциями, он может быть явно добавлен только программистами, например, с
std::numeric_limits::signaling_NaN
Я не смог найти четких кавычек IEEE 754 или C11 для этого, но я также не могу найти встроенную операцию, которая генерирует sNaNs;-)
Однако в руководстве Intel этот принцип четко изложен в разделе 4.8.3.4 "NaNs":
SNaNs обычно используются для перехвата или вызова обработчика исключений. Они должны быть вставлены программным обеспечением; то есть процессор никогда не генерирует 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).
Что делают qNaNs и sNaNs по-разному?
Теперь, когда мы знаем, как выглядят qNaNs и sNaNs и как ими манипулировать, мы наконец-то готовы попытаться заставить 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 обрабатывает и сообщает приложению через сигнал.
В результате Bash печатает 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 и продолжить выполнение
Каковы некоторые реальные применения sNaNs?
Честно говоря, я до сих пор не понял супер полезного варианта использования для sNaNs, об этом спрашивали: Полезность сигнализации NaN?
sNaNs чувствуют себя особенно бесполезными, потому что мы можем обнаружить начальные недействительные операции (0.0f/0.0f
) которые генерируют qNaNs с 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>
предоставить обозначения для МЭК 60559 NaN и бесконечности.
Протестировано в Ubuntu 18.10, GCC 8.2. GitHub upstreams: