Qual è la differenza tra NaN silenzioso e NaN di segnalazione?


96

Ho letto di virgola mobile e ho capito che NaN potrebbe derivare da operazioni. Ma non riesco a capire cosa siano esattamente questi concetti. Qual'è la differenza tra loro?

Quale può essere prodotto durante la programmazione C ++? Come programmatore, potrei scrivere un programma che provoca un sNaN?

Risposte:


68

Quando un'operazione si traduce in un NaN silenzioso, non vi è alcuna indicazione che qualcosa sia insolito finché il programma non controlla il risultato e vede un NaN. Cioè, il calcolo continua senza alcun segnale dall'unità a virgola mobile (FPU) o dalla libreria se la virgola mobile è implementata nel software. Un NaN di segnalazione produrrà un segnale, solitamente sotto forma di eccezione dalla FPU. Se l'eccezione viene generata dipende dallo stato della FPU.

C ++ 11 aggiunge alcuni controlli del linguaggio sull'ambiente a virgola mobile e fornisce metodi standardizzati per creare e testare NaN . Tuttavia, se i controlli sono implementati non è ben standardizzato e le eccezioni a virgola mobile non vengono in genere rilevate allo stesso modo delle eccezioni C ++ standard.

Nei sistemi POSIX / Unix, le eccezioni in virgola mobile vengono in genere rilevate utilizzando un gestore per SIGFPE .


34
In aggiunta a questo: in genere, lo scopo di una segnalazione NaN (sNaN) è per il debug. Ad esempio, gli oggetti a virgola mobile potrebbero essere inizializzati su sNaN. Quindi, se il programma fallisce su uno di essi un valore prima di utilizzarlo, si verificherà un'eccezione quando il programma utilizza sNaN in un'operazione aritmetica. Un programma non produrrà un sNaN inavvertitamente; nessuna normale operazione produce sNaN. Sono creati solo specificamente allo scopo di avere un NaN di segnalazione, non come risultato di alcuna aritmetica.
Eric Postpischil

18
Al contrario, i NaN servono per una programmazione più normale. Possono essere prodotti da operazioni normali quando non c'è un risultato numerico (ad esempio, prendendo la radice quadrata di un numero negativo quando il risultato deve essere reale). Il loro scopo è generalmente quello di consentire all'aritmetica di procedere in modo piuttosto normale. Ad esempio, potresti avere una vasta gamma di numeri, alcuni dei quali rappresentano casi speciali che non possono essere gestiti normalmente. È possibile chiamare una funzione complicata per elaborare questo array, che potrebbe operare sull'aritmetica con la consueta aritmetica, ignorando i NaN. Al termine, separerai i casi speciali per più lavoro.
Eric Postpischil

@wrdieter Grazie, quindi solo la differenza principale è generare eccezione o meno.
JalalJaberi

@EricPostpischil Grazie per l'attenzione alla seconda parte della domanda.
JalalJaberi

@JalalJaberi sì, l'eccezione è la differenza principale
wrdieter il

34

Come appaiono sperimentalmente qNaN e sNaN?

Impariamo prima come identificare se abbiamo un sNaN o un qNaN.

Userò C ++ in questa risposta invece di C perché offre il conveniente std::numeric_limits::quiet_NaNe std::numeric_limits::signaling_NaNche non sono riuscito a trovare in C convenientemente.

Tuttavia non sono riuscito a trovare una funzione per classificare se un NaN è sNaN o qNaN, quindi stampiamo semplicemente i byte grezzi 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));
}

Compila ed esegui:

g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

output sulla mia macchina x86_64:

qnan 7fc00000
snan 7fa00000
 inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
 0/0 ffc00000
sqrt ffc00000

Possiamo anche eseguire il programma su aarch64 con la modalità utente 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

e questo produce lo stesso identico output, suggerendo che più archi implementano strettamente IEEE 754.

A questo punto, se non hai familiarità con la struttura dei numeri in virgola mobile IEEE 754, dai un'occhiata a: Cos'è un numero in virgola mobile subnormale?

In binario alcuni dei valori sopra sono:

     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

Da questo esperimento osserviamo che:

  • qNaN e sNaN sembrano differenziarsi solo dal bit 22: 1 significa silenzioso e 0 significa segnalazione

  • Gli infiniti sono anche abbastanza simili con esponente == 0xFF, ma hanno frazione == 0.

    Per questo motivo, NaNs deve impostare il bit 21 a 1, altrimenti non sarebbe possibile distinguere sNaN dall'infinito positivo!

  • nanf() produce diversi NaN, quindi devono esserci più codifiche possibili:

    7fc00000
    7fc00001
    7fc00002
    

    Poiché nan0è uguale a std::numeric_limits<float>::quiet_NaN(), deduciamo che sono tutti NaN silenziosi diversi.

    La bozza standard C11 N1570 conferma che nanf()genera NaN silenziosi, perché nanfinoltra a strtode 7.22.1.3 "Le funzioni strtod, strtof e strtold" dice:

    Una sequenza di caratteri NAN o NAN (n-char-sequence opt) viene interpretata come un NaN silenzioso, se supportato nel tipo restituito, altrimenti come una parte della sequenza di soggetti che non ha la forma prevista; il significato della sequenza di n caratteri è definito dall'implementazione. 293)

Guarda anche:

Come appaiono i qNaN e i sNaN nei manuali?

IEEE 754 2008 raccomanda che (TODO obbligatorio o facoltativo?):

  • qualsiasi cosa con esponente == 0xFF e frazione! = 0 è un NaN
  • e che il bit della frazione più alta differenzia qNaN da sNaN

ma non sembra dire quale bit sia preferito per differenziare l'infinito da NaN.

6.2.1 "Codifiche NaN in formati binari" dice:

Questa clausola secondaria specifica ulteriormente le codifiche di NaN come stringhe di bit quando sono il risultato di operazioni. Quando codificati, tutti i NaN hanno un bit di segno e uno schema di bit necessari per identificare la codifica come NaN e che ne determina il tipo (sNaN vs. qNaN). I bit rimanenti, che si trovano nel campo del significato finale, codificano il carico utile, che potrebbe essere un'informazione diagnostica (vedere sopra). 34

Tutte le stringhe di bit NaN binarie hanno tutti i bit del campo esponente polarizzato E impostati a 1 (vedere 3.4). Una stringa di bit NaN silenziosa deve essere codificata con il primo bit (d1) del campo di significato finale T che è 1. Una stringa di bit NaN di segnalazione dovrebbe essere codificata con il primo bit del campo di significato finale e 0. Se il primo bit del Il campo significante finale è 0, qualche altro bit del campo significante finale deve essere diverso da zero per distinguere il NaN dall'infinito. Nella codifica preferita appena descritta, una segnalazione NaN deve essere silenziata impostando d1 a 1, lasciando invariati i bit rimanenti di T. Per i formati binari, il carico utile è codificato nei p − 2 bit meno significativi del campo significante finale

Il Manuale dello sviluppatore del software per le architetture Intel 64 e IA-32 - Volume 1 Architettura di base - 253665-056US Settembre 2015 4.8.3.4 "NaNs" conferma che x86 segue IEEE 754 distinguendo NaN e sNaN per la frazione di bit più alta:

L'architettura IA-32 definisce due classi di NaN: NaN silenziosi (QNaN) e NaN di segnalazione (SNaN). Un QNaN è un NaN con la frazione più significativa impostata e SNaN è un NaN con la frazione più significativa chiara.

così come il manuale di riferimento dell'architettura ARM - ARMv8, per il profilo dell'architettura ARMv8-A - DDI 0487C.a A1.4.3 "Formato a virgola mobile a precisione singola":

fraction != 0: Il valore è un NaN ed è un NaN silenzioso o un NaN di segnalazione. I due tipi di NaN si distinguono per il loro bit di frazione più significativo, bit [22]:

  • bit[22] == 0: Il NaN è un NaN di segnalazione. Il bit di segno può assumere qualsiasi valore e i bit di frazione rimanenti possono assumere qualsiasi valore tranne tutti gli zeri.
  • bit[22] == 1: Il NaN è un NaN silenzioso. Il bit di segno e i bit di frazione rimanenti possono assumere qualsiasi valore.

Come vengono generati qNanS e sNaNs?

Una delle principali differenze tra qNaN e sNaN è che:

  • qNaN è generato da normali operazioni aritmetiche integrate (software o hardware) con valori strani
  • sNaN non viene mai generato da operazioni integrate, può essere aggiunto solo esplicitamente dai programmatori, ad esempio con std::numeric_limits::signaling_NaN

Non sono riuscito a trovare citazioni IEEE 754 o C11 chiare per questo, ma non riesco nemmeno a trovare alcuna operazione incorporata che generi sNaN ;-)

Il manuale Intel afferma chiaramente questo principio tuttavia in 4.8.3.4 "NaNs":

Gli SNaN vengono in genere utilizzati per intercettare o richiamare un gestore di eccezioni. Devono essere inseriti da software; ovvero, il processore non genera mai un SNaN come risultato di un'operazione in virgola mobile.

Questo può essere visto dal nostro esempio in cui entrambi:

float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);

producono esattamente gli stessi bit di std::numeric_limits<float>::quiet_NaN().

Entrambe queste operazioni vengono compilate in un'unica istruzione di assembly x86 che genera il qNaN direttamente nell'hardware (TODO confermare con GDB).

Cosa fanno in modo diverso qNaN e sNaN?

Ora che sappiamo che aspetto hanno qNaN e sNaN e come manipolarli, siamo finalmente pronti per provare a fare in modo che i sNaN facciano le loro cose e far saltare in aria alcuni programmi!

Quindi, senza ulteriori indugi:

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;
}

Compila, esegui e ottieni lo stato di uscita:

g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?

Produzione:

FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136

Nota che questo comportamento si verifica solo -O0in GCC 8.2: con -O3, GCC precalcola e ottimizza tutte le nostre operazioni sNaN! Non sono sicuro che esista un modo conforme agli standard per impedirlo.

Quindi deduciamo da questo esempio che:

  • snan + 1.0cause FE_INVALID, ma qnan + 1.0non lo fa

  • Linux genera un segnale solo se è abilitato con feenableexept.

    Questa è un'estensione glibc, non sono riuscito a trovare alcun modo per farlo in nessuno standard.

Quando il segnale si verifica, è perché l'hardware della CPU stesso solleva un'eccezione, che il kernel Linux ha gestito e informato l'applicazione attraverso il segnale.

Il risultato è che bash stampa Floating point exception (core dumped)e lo stato di uscita è 136, che corrisponde a signal 136 - 128 == 8, che secondo:

man 7 signal

è SIGFPE.

Nota che SIGFPEè lo stesso segnale che otteniamo se proviamo a dividere un numero intero per 0:

int main() {
    int i = 1 / 0;
}

sebbene per interi:

  • dividendo qualsiasi cosa per zero si alza il segnale, poiché non c'è rappresentazione dell'infinito in numeri interi
  • il segnale avviene di default, senza necessità feenableexcept

Come gestire il SIGFPE?

Se crei solo un gestore che ritorna normalmente, porta a un ciclo infinito, perché dopo il ritorno del gestore, la divisione si ripete! Questo può essere verificato con GDB.

L'unico modo è usare setjmpe longjmpsaltare da qualche altra parte come mostrato in: C gestire il segnale SIGFPE e continuare l'esecuzione

Quali sono alcune applicazioni del mondo reale di sNaNs?

Onestamente, non ho ancora capito un caso d'uso super utile per sNaNs, questo è stato chiesto a: Utilità di segnalazione NaN?

sNaNs si sentono particolarmente inutili perché possiamo rilevare le operazioni non valide iniziali ( 0.0f/0.0f) che generano qNaNs con feenableexcept: sembra che snansollevi solo errori per più operazioni che qnannon sollevano per, eg ( qnan + 1.0f).

Per esempio:

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);
}

compilare:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm

poi:

./main.out

dà:

Floating point exception (core dumped)

e:

./main.out  1

dà:

f1 -nan
f2 -nan

Vedi anche: Come tracciare un NaN in C ++

Quali sono i flag di segnale e come vengono manipolati?

Tutto è implementato nell'hardware della CPU.

I flag risiedono in alcuni registri, così come il bit che dice se deve essere sollevata un'eccezione / un segnale.

Questi registri sono accessibili dalla userland dalla maggior parte degli archivi.

Questa parte del codice glibc 2.29 è in realtà molto facile da capire!

Ad esempio, fetestexceptè implementato per x86_86 in 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)

quindi vediamo immediatamente che le istruzioni d'uso sono stmxcsrche stanno per "Store MXCSR Register State".

Ed feenableexceptè implementato in 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;
}

Cosa dice lo standard C su qNaN vs sNaN?

La bozza dello standard C11 N1570 afferma esplicitamente che lo standard non fa differenza tra loro in F.2.1 "Infiniti, zeri con segno e NaN":

1 Questa specifica non definisce il comportamento della segnalazione dei NaN. In genere utilizza il termine NaN per indicare NaN silenziosi. Le macro NAN e INFINITY e le funzioni nan <math.h>forniscono designazioni per NaN e infiniti IEC 60559.

Testato in Ubuntu 18.10, GCC 8.2. GitHub a monte:


en.wikipedia.org/wiki/IEEE_754#Interchange_formats sottolinea che IEEE-754 suggerisce semplicemente che 0 per segnalare NaNs è una buona scelta di implementazione, per consentire di silenziare un NaN senza rischiare di renderlo un infinito (significando = 0). Apparentemente non è standardizzato, sebbene sia ciò che fa x86. (E il fatto che sia il MSB del significato che determina qNaN rispetto a sNaN è standardizzato). en.wikipedia.org/wiki/Single-precision_floating-point_format dice x86 e ARM sono gli stessi, ma PA-RISC ha fatto la scelta opposta.
Peter Cordes

@PeterCordes sì, non sono sicuro di cosa "dovrebbe" == "deve" o "è preferito" in IEEE 754 20at "Una stringa di bit NaN di segnalazione dovrebbe essere codificata con il primo bit del campo del significato finale che è 0".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

re: ma non sembra specificare quale bit dovrebbe essere usato per differenziare l'infinito da NaN. Hai scritto che come ti aspettavi che ci fosse un bit specifico che lo standard consiglia di impostare per distinguere sNaN dall'infinito. IDK perché ti aspetteresti che ci sia qualcosa del genere; qualsiasi scelta diversa da zero va bene. Basta scegliere qualcosa che in seguito identifichi da dove proviene sNaN. IDK, suona solo come un fraseggio strano, e la mia prima impressione durante la lettura è stata che stavi dicendo che la pagina web non descriveva ciò che distingue inf da NaN nella codifica (un significato tutto zero).
Peter Cordes

Prima del 2008, IEEE 754 diceva quale fosse il bit di segnalazione / silenzioso (bit 22) ma non quale valore specificava cosa. La maggior parte dei processori convergeva su 1 = silenzioso, quindi nell'edizione 2008 è stato fatto parte dello standard. Dice "dovrebbe" piuttosto che "deve" per evitare di rendere non conformi le implementazioni più vecchie che rendevano la stessa scelta non conforme. In generale, "dovrebbe" in uno standard significa "deve, a meno che tu non abbia ragioni molto convincenti (e preferibilmente ben documentate) per non conformarti".
John Cowan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.