2010-02-11 13 views
46

Recentemente ho letto un bel po 'di IEEE 754 e dell'architettura x87. Stavo pensando di usare NaN come "valore mancante" in un certo codice di calcolo numerico su cui sto lavorando, e speravo che l'utilizzo del segnale NaN mi permettesse di rilevare un'eccezione a virgola mobile nei casi in cui non lo facevo voglio procedere con "valori mancanti". Al contrario, userei il silenzioso NaN per consentire al "valore mancante" di propagarsi attraverso un calcolo. Tuttavia, segnalare i NaN non funziona come pensavo che si basassero sulla documentazione (molto limitata) che esiste su di essi.Utilità di segnalazione NaN?

Ecco un riassunto di quello che so (tutto questo con x87 e VC++):

  • _EM_INVALID (l'eccezione "non valido" IEEE) controlla il comportamento del x87 quando incontra NaNs
  • Se _EM_INVALID è mascherato (l'eccezione è disabilitata), non viene generata alcuna eccezione e le operazioni possono restituire NaN silenzioso. Un'operazione che implica la segnalazione di NaN sarà non causando un'eccezione, ma verrà convertita in NaN silenzioso.
  • Se _EM_INVALID non è mascherato (eccezione abilitata), un'operazione non valida (ad es. Sqrt (-1)) genera un'eccezione non valida.
  • Il x87 mai genera la segnalazione NaN.
  • Se _EM_INVALID viene smascherato, qualsiasi l'uso di un NaN di segnalazione (anche inizializzando una variabile con esso) causa l'eccezione non valida generata.

la libreria standard fornisce un modo per accedere ai valori NaN:

std::numeric_limits<double>::signaling_NaN(); 

e

std::numeric_limits<double>::quiet_NaN(); 

Il problema è che non vedo alcuna utilità per la segnalazione NaN. Se _EM_INVALID è mascherato, si comporta esattamente come NaN silenzioso. Dal momento che nessun NaN è paragonabile a nessun altro NaN, non c'è alcuna differenza logica.

Se _EM_INVALID è non mascherato (eccezione è attivato), quindi non si può nemmeno inizializzare una variabile con una segnalazione NaN: double dVal = std::numeric_limits<double>::signaling_NaN(); perché questo genera un'eccezione (il valore NaN di segnalazione viene caricato in un x87 registrati per memorizzarlo all'indirizzo di memoria).

si potrebbe pensare il seguente come ho fatto io:

  1. Mask _EM_INVALID.
  2. Inizializza la variabile con segnalazione NaN.
  3. Unmask_EM_INVALID.

Tuttavia, il punto 2 provoca la segnalazione NaN da convertire in un NaN tranquilla, saranno non eccezioni motivo di essere gettati in modo da usi successivi di esso! Quindi WTF ?!

Esiste qualche utilità o scopo per un segnale NaN? Capisco che uno degli intenti originali era quello di inizializzare la memoria con essa in modo da poter catturare l'uso di un valore in virgola mobile unitializzato.

Qualcuno può dirmi se mi manca qualcosa qui?


EDIT:

Per illustrare ulteriormente quello che avevo sperato di fare, ecco un esempio:

considerare l'esecuzione di operazioni matematiche su un vettore di dati (doppie). Per alcune operazioni, voglio consentire al vettore di contenere un "valore mancante" (supponiamo che questo corrisponda ad una colonna del foglio di calcolo, ad esempio, in cui alcune celle non hanno un valore, ma la loro esistenza è significativa). Per alcune operazioni, faccio non per consentire al vettore di contenere un "valore mancante". Forse voglio prendere una diversa linea di condotta se un "valore mancante" è presente nell'insieme - magari eseguendo un'operazione diversa (quindi non è uno stato non valido in cui entrare).

Questo codice originale sarebbe simile a questa:

const double MISSING_VALUE = 1.3579246e123; 
using std::vector; 

vector<double> missingAllowed(1000000, MISSING_VALUE); 
vector<double> missingNotAllowed(1000000, MISSING_VALUE); 

// ... populate missingAllowed and missingNotAllowed with (user) data... 

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) { 
    if (*it != MISSING_VALUE) *it = sqrt(*it); // sqrt() could be any operation 
} 

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) { 
    if (*it != MISSING_VALUE) *it = sqrt(*it); 
    else *it = 0; 
} 

Si noti che il controllo per il "valore mancante" deve essere eseguita ogni iterazione del ciclo. Mentre capisco nella maggior parte dei casi, la funzione sqrt (o qualsiasi altra operazione matematica) probabilmente oscurerà questo controllo, ci sono casi in cui l'operazione è minima (forse solo un'aggiunta) e il controllo è costoso. Per non parlare del fatto che il "valore mancante" prende un valore di input legale fuori dal gioco e potrebbe causare errori se un calcolo arriva legittimamente a quel valore (per quanto improbabile possa essere). Anche per essere tecnicamente corretto, i dati di input dell'utente dovrebbero essere controllati rispetto a tale valore e dovrebbe essere intrapresa una linea d'azione appropriata. Trovo questa soluzione inelegante e non ottimale per quanto riguarda le prestazioni. Questo è un codice critico per le prestazioni, e sicuramente non abbiamo il lusso di strutture dati parallele o oggetti di elementi dati di qualche tipo.

versione Nan sarebbe simile a questa:

using std::vector; 

vector<double> missingAllowed(1000000, std::numeric_limits<double>::quiet_NaN()); 
vector<double> missingNotAllowed(1000000, std::numeric_limits<double>::signaling_NaN()); 

// ... populate missingAllowed and missingNotAllowed with (user) data... 

for (vector<double>::iterator it = missingAllowed.begin(); it != missingAllowed.end(); ++it) { 
    *it = sqrt(*it); // if *it == QNaN then sqrt(*it) == QNaN 
} 

for (vector<double>::iterator it = missingNotAllowed.begin(); it != missingNotAllowed.end(); ++it) { 
    try { 
     *it = sqrt(*it); 
    } catch (FPInvalidException&) { // assuming _seh_translator set up 
     *it = 0; 
    } 
} 

Ora il controllo esplicito viene eliminato e la performance dovrebbe essere migliorata. Penso che tutto ciò funzionerebbe se potessi inizializzare il vettore senza toccare i registri FPU ...

Inoltre, immagino che qualsiasi controllo di implementazione sqrt che si rispetti per NaN restituisca immediatamente NaN.

+6

Buona domanda. Sfortunatamente l'unico uso che ho mai visto per segnalare i NaN è quello di generare una chiamata al mio cellulare alle 21:30 di sabato. –

risposta

8

quanto mi risulta, allo scopo di segnalare NaN è inizializzare strutture dati, ma, naturalmente runtime inizializzazione C corre il rischio di avere la NaN caricata in un registro galleggiante come parte di inizializzazione, innescando così la segnale perché il compilatore non è consapevole che questo valore float deve essere copiato utilizzando un registro intero.

Spero che sia possibile inizializzare un valore static con un NaN di segnalazione, ma anche quello richiederebbe una gestione speciale da parte del compilatore per evitare di convertirlo in un NaN silenzioso. Potresti forse usare un po 'di magia cast per evitare di averlo trattato come valore float durante l'inizializzazione.

Se stavate scrivendo in ASM, questo non sarebbe un problema. ma in C e specialmente in C++, penso che dovrai sovvertire il sistema di tipi per inizializzare una variabile con NaN. Suggerisco di utilizzare memcpy.

+1

Sì, penso che possa essere un'ipotesi ragionevole. Penso che dovrebbe essere parte del linguaggio e non solo parte di una libreria perché funzioni come dovrebbe. –

1

L'utilizzo di valori speciali (anche NULL) può rendere i dati molto più confusi e il codice molto più confuso. Sarebbe impossibile distinguere tra un risultato QNaN e un valore "speciale" QNaN.

Potrebbe essere meglio mantenere una struttura dati parallela per tracciare la validità, o forse avere i dati FP in una struttura dati diversa (sparsa) per conservare solo dati validi.

Questo è un consiglio abbastanza generale; i valori speciali sono molto utili in alcuni casi (ad esempio limiti di prestazioni o di memoria davvero ristretti), ma man mano che il contesto aumenta, possono causare più difficoltà di quanto valgano.

+0

È una bella domanda però. Offro questa risposta solo come umile consiglio ai viaggiatori meno esperti che pensano che "sarebbe un bel trucco!" :-) –

+1

Gotcha. Ovviamente è difficile commentare sul codice che non ho visto, e so per esperienza amara com'è lavorare su grandi codebase brutti che ti viene detto di non cambiare, ma data la possibilità di provare a isolare la logica per controlla il valore in una funzione (anche una macro se la controlli così frequentemente una chiamata di funzione non inline rallenterebbe le build di debug). Se non altro rende tutto molto più chiaro, e nella migliore delle ipotesi ti dà la possibilità di riorganizzarlo se trovi una soluzione migliore. –

+0

Ok, ora ha molto più senso. Nel tuo esempio, l'uso di QNaN ed evitare un test sarà molto più SIMD e cache friendly. Come ho detto nella mia risposta originale, i ristretti vincoli di memoria/prestazioni sono un'eccezione alla regola generale di non usare valori speciali :-) Grazie a STingRaySC. Mi dispiace di non avere una risposta per il tuo problema. –

1

Non potresti semplicemente avere un const uint64_t dove i bit sono stati impostati su quelli di un nan di segnalazione? finché lo trattate come un tipo intero, la segnalazione nan non è diversa dagli altri interi. Si potrebbe scrivere dove vuoi tramite puntatore-casting:

Const uint64_t sNan = 0xfff0000000000000; 
Double[] myData; 
... 
Uint64* copier = (uint64_t*) &myData[index]; 
*copier=sNan | myErrorFlags; 

Per info sui bit per impostare: https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html