2010-02-11 19 views
46

Recientemente he leído bastante sobre IEEE 754 y la arquitectura x87. Estaba pensando en usar NaN como un "valor perdido" en algún código de cálculo numérico en el que estoy trabajando, y esperaba que el uso de de señalización NaN me permitiera detectar una excepción de punto flotante en los casos en que no lo hago quiero proceder con "valores perdidos". Por el contrario, usaría NaN para permitir que el "valor perdido" se propague a través de un cálculo. Sin embargo, los NaN de señalización no funcionan como pensé que basarían en la documentación (muy limitada) que existe sobre ellos.Utilidad de la señalización NaN?

Aquí se presenta un resumen de lo que sé (todo esto utilizando x87 y VC++):

  • _EM_INVALID (IEEE excepción "no válido") controla el comportamiento de la x87 cuando se enfrentan a NaNs
  • Si _EM_INVALID está enmascarado (la excepción está deshabilitada), no se genera ninguna excepción y las operaciones pueden devolver NaN silencioso. Una operación que implica la señalización de NaN será no causará una excepción, pero se convertirá en NaN silencioso.
  • Si _EM_INVALID se desenmascara (excepción habilitada), una operación no válida (por ejemplo, sqrt (-1)) provoca una excepción no válida.
  • El x87 nunca genera NaN de señalización.
  • Si _EM_INVALID se desenmascara, cualquier uso de un NaN de señalización (incluso inicializando una variable con él) provoca una excepción no válida que se lanzará.

la biblioteca estándar proporciona una manera de acceder a los valores NaN:

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

y

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

El problema es que no veo utilidad alguna para la señalización NaN. Si _EM_INVALID está enmascarado se comporta exactamente igual que NaN silencioso. Como no NaN es comparable a cualquier otro NaN, no hay diferencia lógica.

Si _EM_INVALID es no enmascarado (excepción está activado), entonces ni siquiera se puede inicializar una variable con una señalización NaN: double dVal = std::numeric_limits<double>::signaling_NaN(); porque esto produce una excepción (el valor NaN de señalización se carga en un x87 registro para almacenarlo a la dirección de memoria).

usted puede pensar lo siguiente como lo hice:

  1. _EM_INVALID máscara.
  2. Inicialice la variable con NaN de señalización.
  3. Unmask_EM_INVALID.

Sin embargo, el paso 2 hace que la señalización NaN a convertir en una tranquila NaN, se no excepciones causa que se arrojen usos posteriores por lo que de ella! Entonces WTF ?!

¿Hay alguna utilidad o propósito en absoluto para un NaN de señalización? Entiendo que una de las intenciones originales fue inicializar la memoria con ella para poder capturar el uso de un valor de coma flotante unificado.

¿Puede alguien decirme si me falta algo aquí?


EDIT:

Para ilustrar mejor lo que había esperado hacer, aquí es un ejemplo:

Considere realizar operaciones matemáticas en un vector de datos (dobles). Para algunas operaciones, quiero permitir que el vector contenga un "valor perdido" (pretender que corresponde a una columna de hoja de cálculo, por ejemplo, en la que algunas de las celdas no tienen un valor, pero su existencia es significativa). Para algunas operaciones, hago no quiero permitir que el vector contenga un "valor faltante". Quizás quiera tomar un curso de acción diferente si hay un "valor perdido" en el conjunto, tal vez realizando una operación diferente (por lo tanto, este no es un estado inválido).

Este código original sería algo como esto:

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

Tenga en cuenta que el cheque por el "valor faltante" debe realizarse cada repetición del bucle. Aunque entiendo en la mayoría de los casos, la función sqrt (o cualquier otra operación matemática) probablemente eclipsará esta verificación, hay casos donde la operación es mínima (quizás solo una adición) y el cheque es costoso. Sin mencionar el hecho de que el "valor perdido" tiene un valor de entrada legal fuera de juego y podría causar errores si un cálculo llega legítimamente a ese valor (por improbable que sea). Además, para ser técnicamente correcto, los datos de entrada del usuario se deben comparar con ese valor y se debe tomar un curso de acción apropiado. Encuentro esta solución poco elegante y menos que óptima en cuanto a rendimiento. Este es un código de rendimiento crítico, y definitivamente no tenemos el lujo de estructuras de datos paralelas u objetos de elementos de datos de algún tipo.

versión Nan se vería así:

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

Ahora la comprobación explícita se elimina y el rendimiento debe ser mejorado. Creo que todo esto funcionaría si pudiera inicializar el vector sin tocar los registros de la FPU ...

Además, me imagino que cualquier implementación que se precie de sqrt verifica NaN y devuelve NaN inmediatamente.

+6

Buena pregunta. Desafortunadamente, el único uso que he visto para señalar NaNs es generar una llamada a mi teléfono celular a las 9:30 PM del sábado. –

Respuesta

8

Como lo entiendo, el propósito de señalización NaN es inicializar estructuras de datos, pero, por supuesto tiempo de ejecución inicialización en C corre el riesgo de tener el NaN cargado en un registro de flotador como parte de la inicialización, iniciando así el señal porque el compilador no es consciente de que este valor flotante debe copiarse usando un registro entero.

Espero que pueda inicializar un valor static con un NaN de señalización, pero incluso eso requeriría un manejo especial por parte del compilador para evitar que se convierta en un NaN silencioso. Quizás puedas usar un poco de magia de lanzamiento para evitar que se trate como un valor flotante durante la inicialización.

Si estuviera escribiendo en ASM, esto no sería un problema. pero en C y especialmente en C++, creo que tendrá que subvertir el sistema de tipos para inicializar una variable con NaN. Sugiero usar memcpy.

+1

Sí, creo que puede ser una suposición razonable. Creo que debería ser parte del lenguaje y no solo parte de una biblioteca para que funcione de la manera que se supone. –

1

Usar valores especiales (incluso NULL) puede hacer que sus datos sean mucho más confusos y su código mucho más desordenado. Sería imposible distinguir entre un resultado QNaN y un valor QNaN "especial".

Puede que sea mejor mantener una estructura de datos paralela para rastrear la validez, o tal vez tener sus datos de FP en una estructura de datos diferente (escasa) para mantener solo datos válidos.

Este es un consejo bastante general; los valores especiales son muy útiles en ciertos casos (por ejemplo, restricciones de rendimiento o memoria muy ajustadas), pero a medida que el contexto aumenta, pueden causar más dificultades de las que valen.

+0

Sin embargo, es una buena pregunta. Solo ofrezco esta respuesta como un humilde consejo para viajeros menos experimentados que piensan que "¡sería un buen truco!" :-) –

+1

Gotcha. Obviamente, es difícil comentar sobre el código que no he visto, y sé por amarga experiencia cómo es trabajar en grandes bases de datos desagradables que se te dice que no debes cambiar, pero dada la posibilidad, trataría de aislar la lógica de compruebe el valor de una función (incluso una macro si la comprueba con tanta frecuencia que una llamada a una función no inline ralentizaría las compilaciones de depuración). Si nada más lo hace todo mucho más claro, y en el mejor de los casos te da la oportunidad de reorganizarlo si encuentras una mejor solución. –

+0

Ok, eso tiene mucho más sentido ahora. En su ejemplo, usar QNaN y evitar una prueba será mucho más SimD y compatible con la caché. Como dije en mi respuesta original, las estrechas restricciones de memoria/rendimiento son una excepción a la regla general de no usar valores especiales :-) Gracias STingRaySC. Lo siento, no tengo una respuesta para tu problema. –

1

¿No podría simplemente tener un const uint64_t donde los bits se han establecido a los de un nan de señalización? siempre que lo trate como un tipo entero, la señalización nan no es diferente de otros enteros. Se puede escribir el lugar que desee a través del puntero de fundición:

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

Para información sobre los bits para establecer: https://www.doc.ic.ac.uk/~eedwards/compsys/float/nan.html