Kiedy operacja skutkuje cichym NaN, nic nie wskazuje na to, że coś jest niezwykłe, dopóki program nie sprawdzi wyniku i nie zobaczy NaN. Oznacza to, że obliczenia są kontynuowane bez żadnego sygnału z jednostki zmiennoprzecinkowej (FPU) lub biblioteki, jeśli zmiennoprzecinkowe są zaimplementowane w oprogramowaniu. Sygnalizujący NaN wytworzy sygnał, zwykle w postaci wyjątku od FPU. To, czy wyjątek zostanie zgłoszony, zależy od stanu FPU.
C ++ 11 dodaje kilka kontroli języka w środowisku zmiennoprzecinkowym i zapewnia standardowe sposoby tworzenia i testowania NaN . Jednak to, czy kontrolki są zaimplementowane, nie jest dobrze znormalizowane, a wyjątki zmiennoprzecinkowe zwykle nie są przechwytywane w taki sam sposób, jak standardowe wyjątki C ++.
W systemach POSIX / Unix wyjątki zmiennoprzecinkowe są zwykle przechwytywane przy użyciu procedury obsługi dla SIGFPE .
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
istd::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
Skompiluj i uruchom:
dane wyjściowe na mojej maszynie x86_64:
Możemy również uruchomić program na aarch64 w trybie użytkownika QEMU:
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:
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ń:Ponieważ
nan0
jest to to samo costd::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ż wnanf
przód dostrtod
i 7.22.1.3 „Funkcje strtod, strtof i strtold” mówi:Zobacz też:
Jak qNaNs i SNaNs wyglądają w instrukcjach?
IEEE 754 2008 zaleca, aby (TODO obowiązkowe czy opcjonalne?):
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:
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:
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:
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”:
Można to zobaczyć na naszym przykładzie, w którym oba:
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
Skompiluj, uruchom i uzyskaj status wyjścia:
Wynik:
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
powodujeFE_INVALID
, aleqnan + 1.0
tak nie jestLinux 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 to136
, co odpowiada sygnałowi136 - 128 == 8
, który zgodnie z:jest
SIGFPE
.Zauważ, że
SIGFPE
jest to ten sam sygnał, który otrzymujemy, jeśli spróbujemy podzielić liczbę całkowitą przez 0:chociaż dla liczb całkowitych:
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
ilongjmp
przeskoczenie gdzie indziej, jak pokazano w: C uchwyt sygnału SIGFPE i kontynuowanie wykonywaniaJakie 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ę, żesnan
po prostu wywołuje błędy dla większej liczby operacji, któreqnan
nie powodują, np. (qnan + 1.0f
).Na przykład:
main.c
skompilować:
następnie:
daje:
i:
daje:
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 :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 :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”:
Testowane w Ubuntu 18.10, GCC 8.2. GitHub upstreams:
źródło