c++ visual-c++ floating-point ieee-754 x87

c++ - Utilidad de la señalización de NaN?



visual-c++ floating-point (3)

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 NaN de señalización me permitiera detectar una excepción de punto flotante en los casos en que no quiero proceder con "valores perdidos". Por el contrario, usaría NaN silencioso 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.

Aquí hay un resumen de lo que sé (todo esto usando x87 y VC ++):

  • _EM_INVALID (la excepción IEEE "no válida") controla el comportamiento del x87 al encontrarse con NaN
  • 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 no provocará una excepción, sino que se convertirá a NaN silencioso.
  • Si _EM_INVALID se desenmascara (la excepción está habilitada), una operación no válida (p. Ej., Sqrt (-1)) provoca la emisión de una excepción no válida.
  • El x87 nunca genera señal NaN.
  • Si se desenmascara _EM_INVALID, cualquier uso de un NaN de señalización (incluso inicializando una variable con él) provoca la emisión de una excepción no válida.

La biblioteca estándar proporciona una forma 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 uso alguno para la señalización de 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 no está enmascarado (la excepción está habilitada), entonces uno ni siquiera puede inicializar una variable con un NaN de señalización: double dVal = std::numeric_limits<double>::signaling_NaN(); porque esto arroja una excepción (el valor NaN de señalización se carga en un registro x87 para almacenarlo en la dirección de memoria).

Usted puede pensar lo siguiente como lo hice:

  1. Máscara _EM_INVALID.
  2. Inicializa la variable con NaN de señalización.
  3. Desenmascarar_EM_INVALID.

Sin embargo, el paso 2 hace que el NaN de señalización se convierta en un NaN silencioso, por lo que los usos posteriores de él no provocarán excepciones. Entonces WTF ?!

¿Hay alguna utilidad o propósito de señalizar NaN? 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í?

EDITAR:

Para ilustrar mejor lo que esperaba hacer, aquí hay 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, no quiero permitir que el vector contenga un "valor perdido". 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 se vería así:

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 la verificación del "valor faltante" se debe realizar en cada iteración de 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.

La versión de 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 se elimina la comprobación explícita y se debe mejorar el rendimiento. Creo que todo esto funcionaría si pudiera inicializar el vector sin tocar los registros FPU ...

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


¿No podrías 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. Puedes escribirlo donde quieras a través del puntero:

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

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


El uso de 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".

Es posible 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 ej., memoria muy ajustada o limitaciones de rendimiento), pero a medida que el contexto aumenta, pueden causar más dificultades de las que valen.


Según entiendo, el propósito de señalizar NaN es inicializar las estructuras de datos, pero, por supuesto, la inicialización del tiempo de ejecución en C corre el riesgo de tener el NaN cargado en un registro flotante como parte de la inicialización, activando la señal porque el compilador no está No es consciente de que este valor flotante debe copiarse usando un registro entero.

Espero que puedas 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 estuvieras 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 .