Jaka jest różnica między cichym NaN a sygnalizującym NaN?

106

Czytałem o zmiennoprzecinkowych i rozumiem, że NaN może wynikać z operacji. Ale nie mogę dokładnie zrozumieć, co to są koncepcje. Jaka jest różnica między nimi?

Który z nich można stworzyć podczas programowania w C ++? Czy jako programista mógłbym napisać program, który powoduje sNaN?

JalalJaberi
źródło

Odpowiedzi:

72

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 .

wrdieter
źródło
36
Dodając do tego: Generalnie celem sygnalizacji NaN (sNaN) jest debugowanie. Na przykład obiekty zmiennoprzecinkowe mogą zostać zainicjowane do sNaN. Następnie, jeśli program nie poda jednej z nich wartości przed jej użyciem, wystąpi wyjątek, gdy program użyje sNaN w operacji arytmetycznej. Program nie wytworzy nieumyślnie sNaN; Żadna normalna operacja nie generuje sNaNs. Są tworzone wyłącznie w celu posiadania sygnalizacyjnego NaN, a nie w wyniku jakiejkolwiek arytmetyki.
Eric Postpischil
19
Natomiast NaNs służą do bardziej normalnego programowania. Można je wygenerować za pomocą zwykłych operacji, gdy nie ma wyniku liczbowego (np. Biorąc pierwiastek kwadratowy z liczby ujemnej, gdy wynik musi być rzeczywisty). Ich celem jest ogólnie umożliwienie wykonywania arytmetyki nieco normalnego działania. Na przykład możesz mieć ogromną tablicę liczb, z których niektóre reprezentują specjalne przypadki, których nie można normalnie obsłużyć. Możesz wywołać skomplikowaną funkcję do przetwarzania tej tablicy i może ona działać na tablicy przy użyciu zwykłej arytmetyki, ignorując NaN. Po jego zakończeniu należy oddzielić przypadki specjalne, aby uzyskać więcej pracy.
Eric Postpischil
@wrdieter Dziękuję, to tylko największa różnica generuje wyjątek lub nie.
JalalJaberi
@EricPostpischil Dziękuję za uwagę na drugie pytanie.
JalalJaberi
@JalalJaberi tak, wyjątkiem jest główna różnica
wrdieter
39

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_NaNi std::numeric_limits::signaling_NaNktó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ż nan0jest 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 nanfprzód do strtodi 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 -O0w 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.0powoduje FE_INVALID, ale qnan + 1.0tak 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 SIGFPEjest 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 setjmpi longjmpprzeskoczenie 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 snanpo prostu wywołuje błędy dla większej liczby operacji, które qnannie 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 fetestexceptjest 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 stmxcsrskrót od „Store MXCSR Register State”.

I feenableexceptjest 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:

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
źródło
en.wikipedia.org/wiki/IEEE_754#Interchange_formats wskazuje, że IEEE-754 jedynie sugeruje, że 0 do sygnalizacji NaN jest dobrym wyborem implementacyjnym, aby umożliwić wyciszenie NaN bez ryzyka uczynienia go nieskończonością (istotność = 0). Najwyraźniej to nie jest ustandaryzowane, chociaż tak właśnie robi x86. (A fakt, że to MSB znaczenia, który określa qNaN vs sNaN, jest znormalizowany). en.wikipedia.org/wiki/Single-precision_floating-point_format mówi, że x86 i ARM są takie same, ale PA-RISC dokonał innego wyboru.
Peter Cordes,
@PeterCordes tak, nie jestem pewien, co „powinno” == „musi” lub „jest preferowane” w IEEE 754 20at „Sygnalizujący ciąg bitów NaN powinien być zakodowany z pierwszym bitem końcowego pola znacznika o wartości 0”.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
re: ale nie wydaje się określać, który bit powinien być użyty do odróżnienia nieskończoności od NaN. Napisałeś, że tak jak spodziewałeś się, że będzie jakiś specyficzny bit, który standard zaleca ustawienie, aby odróżnić sNaN od nieskończoności. IDK, dlaczego spodziewałbyś się, że będzie taki bit; każdy wybór niezerowy jest w porządku. Po prostu wybierz coś, co później określi, skąd pochodzi sNaN. IDK, po prostu brzmi jak dziwne sformułowanie, a moje pierwsze wrażenie podczas czytania było takie, że powiedziałeś, że strona internetowa nie opisuje tego, co odróżnia inf od NaN w kodowaniu (znaczenie zerowe).
Peter Cordes,
Przed 2008 rokiem IEEE 754 powiedział, który jest bitem sygnalizacyjnym / cichym (bit 22), ale nie określa, która wartość określa co. Większość procesorów zbiegła się na 1 = cicho, więc stało się to częścią standardu w edycji 2008. Mówi raczej „powinien” niż „musi”, aby uniknąć uczynienia starszych implementacji, które uczyniły ten sam wybór niezgodnym. Ogólnie rzecz biorąc, „powinien” w normie oznacza „musi, chyba że masz bardzo istotne (i najlepiej dobrze udokumentowane) powody, dla których nie zastosujesz się”.
John Cowan