Как 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: