Jak eksperymentalnie wyglądają qNaN i SNaN?
Najpierw nauczmy się, jak rozpoznać, czy mamy sNaN czy qNaN.
W tej odpowiedzi będę używał C ++ zamiast C, ponieważ oferuje to wygodne std::numeric_limits::quiet_NaN
i std::numeric_limits::signaling_NaN
których nie mogłem znaleźć w C.
Nie mogłem jednak znaleźć funkcji do sklasyfikowania, czy NaN to sNaN czy qNaN, więc wydrukujmy po prostu nieprzetworzone bajty 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));
}
Skompiluj i uruchom:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
dane wyjściowe na mojej maszynie x86_64:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Możemy również uruchomić program na aarch64 w trybie użytkownika 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
co daje dokładnie ten sam wynik, co sugeruje, że wiele łuków ściśle implementuje IEEE 754.
W tym momencie, jeśli nie znasz struktury liczb zmiennoprzecinkowych IEEE 754, spójrz na: Co to jest podnormalna liczba zmiennoprzecinkowa?
Binarnie niektóre z powyższych wartości to:
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
Z tego eksperymentu obserwujemy, że:
qNaN i sNaN wydają się być rozróżniane tylko przez bit 22: 1 oznacza cicho, a 0 oznacza sygnalizację
nieskończoności są również dość podobne z wykładnikiem == 0xFF, ale mają ułamek == 0.
Z tego powodu NaN musi ustawić bit 21 na 1, w przeciwnym razie nie byłoby możliwe odróżnienie sNaN od dodatniej nieskończoności!
nanf()
tworzy kilka różnych NaN, więc musi istnieć wiele możliwych kodowań:
7fc00000
7fc00001
7fc00002
Ponieważ nan0
jest to to samo co std::numeric_limits<float>::quiet_NaN()
, wnioskujemy, że wszystkie są różnymi cichymi NaN.
Wersja robocza standardu C11 N1570 potwierdza, że nanf()
generuje ciche NaN, ponieważ w nanf
przód do strtod
i 7.22.1.3 „Funkcje strtod, strtof i strtold” mówi:
Sekwencja znaków NAN lub NAN (n-char-sequence opt) jest interpretowana jako cicha NaN, jeśli jest obsługiwana w zwracanym typie, inaczej jak część sekwencji podmiotu, która nie ma oczekiwanej postaci; znaczenie sekwencji n-char jest zdefiniowane przez implementację. 293)
Zobacz też:
Jak qNaNs i SNaNs wyglądają w instrukcjach?
IEEE 754 2008 zaleca, aby (TODO obowiązkowe czy opcjonalne?):
- wszystko z wykładnikiem == 0xFF i ułamkiem! = 0 jest NaN
- i że najwyższy bit ułamka odróżnia qNaN od sNaN
ale nie wydaje się mówić, który bit jest preferowany, aby odróżnić nieskończoność od NaN.
6.2.1 „Kodowanie NaN w formatach binarnych” mówi:
Ten podrozdział dodatkowo określa kodowanie NaN jako ciągów bitowych, gdy są one wynikiem operacji. Po zakodowaniu wszystkie NaN mają bit znaku i wzorzec bitów niezbędnych do zidentyfikowania kodowania jako NaN i który określa jego rodzaj (sNaN vs. qNaN). Pozostałe bity, które znajdują się w końcowym polu istotności, kodują ładunek, który może być informacją diagnostyczną (patrz wyżej). 34
Wszystkie binarne ciągi bitów NaN mają wszystkie bity polaryzowanego pola wykładnika E ustawione na 1 (patrz 3.4). Cichy ciąg bitów NaN powinien być zakodowany tak, aby pierwszy bit (d1) końcowego pola znacznika T wynosił 1. Sygnalizujący ciąg bitów NaN powinien być zakodowany z pierwszym bitem końcowego pola znacznika równym 0. Jeśli pierwszy bit końcowe pole istotności ma wartość 0, jakiś inny bit końcowego pola istotności musi być różny od zera, aby odróżnić NaN od nieskończoności. W opisanym właśnie korzystnym kodowaniu sygnalizacyjny NaN będzie wyciszany przez ustawienie d1 na 1, pozostawiając pozostałe bity T niezmienione. W przypadku formatów binarnych ładunek jest kodowany w p-2 najmniej znaczących bitach końcowego pola istotności
The Intel 64 i IA-32 architektury Software Developer Obsługi - Volume 1 Podstawowe Architektura - 253665-056US września 2015 4.8.3.4 "Nans" potwierdza, że x86 następująco IEEE 754, wyróżniając NaN i sNaN przez najwyższego bitu frakcji:
Architektura IA-32 definiuje dwie klasy NaN: ciche NaN (QNaN) i sygnalizacyjne NaN (SNaN). QNaN to NaN z ustawionym najbardziej znaczącym bitem ułamkowym, SNaN to NaN z czystym najbardziej znaczącym bitem ułamkowym.
podobnie jest w Podręczniku architektury ARM - ARMv8, dla profilu architektury ARMv8-A - DDI 0487C.a A1.4.3 „Format zmiennoprzecinkowy pojedynczej precyzji”:
fraction != 0
: Wartość to NaN i jest to albo cichy NaN, albo sygnalizujący NaN. Te dwa typy NaN są rozróżniane przez ich najbardziej znaczący bit ułamkowy, bit [22]:
bit[22] == 0
: NaN jest sygnalizującym NaN. Bit znaku może przyjąć dowolną wartość, a pozostałe bity ułamkowe mogą przyjąć dowolną wartość oprócz wszystkich zer.
bit[22] == 1
: NaN to cichy NaN. Bit znaku i pozostałe bity ułamkowe mogą mieć dowolną wartość.
Jak generowane są qNanS i sNaNs?
Jedną z głównych różnic między qNaNs i sNaNs jest to, że:
- qNaN jest generowany przez regularne wbudowane (programowe lub sprzętowe) operacje arytmetyczne z dziwnymi wartościami
- sNaN nigdy nie jest generowany przez operacje wbudowane, może być dodany jawnie tylko przez programistów, np. za pomocą
std::numeric_limits::signaling_NaN
Nie mogłem znaleźć dla tego jasnych cytatów IEEE 754 lub C11, ale nie mogę też znaleźć żadnej wbudowanej operacji, która generuje SNaN ;-)
Podręcznik Intela jasno określa tę zasadę w 4.8.3.4 „NaNs”:
SNaN są zwykle używane do przechwytywania lub wywoływania procedury obsługi wyjątków. Muszą być wstawiane przez oprogramowanie; to znaczy procesor nigdy nie generuje SNaN w wyniku operacji zmiennoprzecinkowej.
Można to zobaczyć na naszym przykładzie, w którym oba:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
produkują dokładnie takie same bity, jak std::numeric_limits<float>::quiet_NaN()
.
Obie te operacje kompilują się do pojedynczej instrukcji asemblera x86, która generuje qNaN bezpośrednio w sprzęcie (TODO potwierdzić z GDB).
Co robią qNaNs i sNaNs inaczej?
Teraz, gdy wiemy, jak wyglądają qNaNs i sNaNs i jak nimi manipulować, jesteśmy wreszcie gotowi, aby spróbować zmusić sNaN do robienia swoich rzeczy i wysadzić kilka programów w powietrze!
Więc bez zbędnych ceregieli:
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;
}
Skompiluj, uruchom i uzyskaj status wyjścia:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Wynik:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Zauważ, że takie zachowanie ma miejsce tylko -O0
w GCC 8.2: z -O3
, GCC wstępnie oblicza i optymalizuje wszystkie nasze operacje sNaN! Nie jestem pewien, czy istnieje zgodny ze standardami sposób zapobiegania temu.
Z tego przykładu wnioskujemy, że:
snan + 1.0
powoduje FE_INVALID
, ale qnan + 1.0
tak nie jest
Linux generuje sygnał tylko wtedy, gdy jest włączony za pomocą feenableexept
.
To jest rozszerzenie glibc, nie mogłem znaleźć sposobu, aby to zrobić w żadnym standardzie.
Kiedy sygnał się pojawia, dzieje się tak, ponieważ sprzęt procesora sam zgłasza wyjątek, który jądro Linuksa obsłużył i poinformował aplikację za pośrednictwem sygnału.
W rezultacie bash drukuje Floating point exception (core dumped)
, a status wyjścia to 136
, co odpowiada sygnałowi 136 - 128 == 8
, który zgodnie z:
man 7 signal
jest SIGFPE
.
Zauważ, że SIGFPE
jest to ten sam sygnał, który otrzymujemy, jeśli spróbujemy podzielić liczbę całkowitą przez 0:
int main() {
int i = 1 / 0;
}
chociaż dla liczb całkowitych:
- dzielenie czegokolwiek przez zero podnosi sygnał, ponieważ w liczbach całkowitych nie ma reprezentacji nieskończoności
- sygnał dzieje się to domyślnie, bez potrzeby
feenableexcept
Jak radzić sobie z SIGFPE?
Jeśli po prostu utworzysz procedurę obsługi, która powraca normalnie, prowadzi to do nieskończonej pętli, ponieważ po powrocie funkcji dzielenie następuje ponownie! Można to zweryfikować za pomocą GDB.
Jedynym sposobem jest użycie setjmp
i longjmp
przeskoczenie gdzie indziej, jak pokazano w: C uchwyt sygnału SIGFPE i kontynuowanie wykonywania
Jakie są rzeczywiste zastosowania SNaNs?
Całkiem szczerze, nadal nie zrozumiałem bardzo użytecznego przypadku użycia sNaNs, zostało to zadane pod adresem: Przydatność sygnalizowania NaN?
sNaNs wydają się szczególnie bezużyteczne, ponieważ możemy wykryć początkowe nieprawidłowe operacje ( 0.0f/0.0f
), które generują qNaN za pomocą feenableexcept
: wydaje się, że snan
po prostu wywołuje błędy dla większej liczby operacji, które qnan
nie powodują, np. ( qnan + 1.0f
).
Na przykład:
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);
}
skompilować:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
następnie:
./main.out
daje:
Floating point exception (core dumped)
i:
./main.out 1
daje:
f1 -nan
f2 -nan
Zobacz także: Jak śledzić NaN w C ++
Czym są flagi sygnałowe i jak się nimi manipuluje?
Wszystko jest zaimplementowane w sprzęcie CPU.
Flagi żyją w jakimś rejestrze, podobnie jak bit, który mówi, czy wyjątek / sygnał powinien zostać zgłoszony.
Te rejestry są dostępne z obszaru użytkownika z większości archów.
Ta część kodu glibc 2.29 jest właściwie bardzo łatwa do zrozumienia!
Na przykład fetestexcept
jest zaimplementowany dla x86_86 w 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)
więc od razu widzimy, że w instrukcji jest stmxcsr
skrót od „Store MXCSR Register State”.
I feenableexcept
jest zaimplementowany w 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;
}
Co standard C mówi o qNaN vs sNaN?
C11 N1570 standardowy projekt wyraźnie mówi, że standard nie rozróżnia między nimi F.2.1 „nieskończoności, podpisanych zer i Nans”:
1 Ta specyfikacja nie definiuje zachowania sygnalizacyjnych NaN. Zwykle używa terminu NaN do oznaczenia cichych NaN. Makra NAN i INFINITY oraz funkcje nan w programie <math.h>
zapewniają oznaczenia dla NaN i nieskończoności IEC 60559.
Testowane w Ubuntu 18.10, GCC 8.2. GitHub upstreams: