Qual a aparência dos qNaNs e sNaNs experimentalmente?
Vamos primeiro aprender como identificar se temos um sNaN ou um qNaN.
Usarei C ++ nesta resposta em vez de C porque oferece o conveniente std::numeric_limits::quiet_NaN
e std::numeric_limits::signaling_NaN
que não pude encontrar em C convenientemente.
No entanto, não consegui encontrar uma função para classificar se um NaN é sNaN ou qNaN, então vamos apenas imprimir os bytes brutos de 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));
}
Compile e execute:
g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out
saída na minha máquina x86_64:
qnan 7fc00000
snan 7fa00000
inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
0/0 ffc00000
sqrt ffc00000
Também podemos executar o programa em aarch64 com o modo de usuário 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 isso produz exatamente a mesma saída, sugerindo que vários archs implementam de perto o IEEE 754.
Neste ponto, se você não estiver familiarizado com a estrutura dos números de ponto flutuante IEEE 754, dê uma olhada em: O que é um número de ponto flutuante subnormal?
Em binário, alguns dos valores acima são:
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
A partir desta experiência, observamos que:
qNaN e sNaN parecem ser diferenciados apenas pelo bit 22: 1 significa silencioso e 0 significa sinalização
infinitos também são bastante semelhantes com expoente == 0xFF, mas eles têm fração == 0.
Por esta razão, os NaNs devem definir o bit 21 para 1, caso contrário não seria possível distinguir sNaN de infinito positivo!
nanf()
produz vários NaNs diferentes, portanto, deve haver várias codificações possíveis:
7fc00000
7fc00001
7fc00002
Como nan0
é o mesmo que std::numeric_limits<float>::quiet_NaN()
, deduzimos que são todos NaNs silenciosos diferentes.
O projeto padrão C11 N1570 confirma que nanf()
gera NaNs silenciosos, porque nanf
encaminha para strtod
e 7.22.1.3 "As funções strtod, strtof e strtold" diz:
Uma sequência de caracteres NAN ou NAN (n-char-sequence opt) é interpretada como um NaN silencioso, se suportado no tipo de retorno, senão como uma parte da sequência de assunto que não tem a forma esperada; o significado da sequência n-char é definido pela implementação. 293)
Veja também:
Qual a aparência dos qNaNs e sNaNs nos manuais?
IEEE 754 2008 recomenda que (TODO obrigatório ou opcional?):
- qualquer coisa com expoente == 0xFF e fração! = 0 é um NaN
- e que o bit de fração mais alto diferencia qNaN de sNaN
mas não parece dizer qual bit é preferido para diferenciar o infinito de NaN.
6.2.1 "Codificações NaN em formatos binários" diz:
Esta subseção especifica ainda mais as codificações de NaNs como cadeias de bits quando são o resultado de operações. Quando codificados, todos os NaNs possuem um bit de sinal e um padrão de bits necessários para identificar a codificação como um NaN e que determina seu tipo (sNaN vs. qNaN). Os bits restantes, que estão no campo de significando à direita, codificam a carga útil, que pode ser uma informação de diagnóstico (veja acima). 34
Todas as cadeias binárias de bits NaN têm todos os bits do campo expoente polarizado E definido como 1 (consulte 3.4). Uma string de bits NaN silenciosa deve ser codificada com o primeiro bit (d1) do campo de significando final T sendo 1. Uma string de bits NaN de sinalização deve ser codificada com o primeiro bit do campo de significando final sendo 0. Se o primeiro bit do O campo de significando final é 0, algum outro bit do campo de significando final deve ser diferente de zero para distinguir o NaN do infinito. Na codificação preferida que acabamos de descrever, uma sinalização NaN deve ser silenciada, definindo d1 para 1, deixando os bits restantes de T inalterados. Para formatos binários, a carga útil é codificada nos p-2 bits menos significativos do campo de significando à direita
O Manual do Intel 64 e IA-32 Arquiteturas Software Developer - Volume 1 arquitetura básica - 253665-056US setembro 2015 4.8.3.4 "NANS" confirma que x86 segue IEEE 754, distinguindo NaN e Snan pelo mais alto bit fração:
A arquitetura IA-32 define duas classes de NaNs: NaNs silenciosos (QNaNs) e NaNs de sinalização (SNaNs). Um QNaN é um NaN com o bit de fração mais significativo definido e um SNaN é um NaN com o bit de fração mais significativo limpo.
e também o ARM Architecture Reference Manual - ARMv8, para o perfil de arquitetura ARMv8-A - DDI 0487C.a A1.4.3 "Formato de ponto flutuante de precisão única":
fraction != 0
: O valor é um NaN e é um NaN silencioso ou um NaN de sinalização. Os dois tipos de NaN são distinguidos por seu bit de fração mais significativo, bit [22]:
bit[22] == 0
: O NaN é um NaN de sinalização. O bit de sinal pode assumir qualquer valor e os bits de fração restantes podem assumir qualquer valor, exceto zeros.
bit[22] == 1
: O NaN é um NaN silencioso. O bit de sinal e os bits de fração restantes podem assumir qualquer valor.
Como qNanS e sNaNs são gerados?
Uma das principais diferenças entre qNaNs e sNaNs é que:
- qNaN é gerado por operações aritméticas internas regulares (software ou hardware) com valores estranhos
- sNaN nunca é gerado por operações integradas, ele só pode ser explicitamente adicionado por programadores, por exemplo, com
std::numeric_limits::signaling_NaN
Não consegui encontrar citações IEEE 754 ou C11 claras para isso, mas também não consigo encontrar nenhuma operação integrada que gere sNaNs ;-)
O manual da Intel afirma claramente este princípio em 4.8.3.4 "NaNs":
SNaNs são normalmente usados para interceptar ou chamar um manipulador de exceção. Devem ser inseridos por software; ou seja, o processador nunca gera um SNaN como resultado de uma operação de ponto flutuante.
Isso pode ser visto em nosso exemplo em que:
float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);
produz exatamente os mesmos bits que std::numeric_limits<float>::quiet_NaN()
.
Ambas as operações são compiladas em uma única instrução de montagem x86 que gera o qNaN diretamente no hardware (TODO confirma com GDB).
O que os qNaNs e os sNaNs fazem de maneira diferente?
Agora que sabemos como são os qNaNs e sNaNs e como manipulá-los, estamos finalmente prontos para tentar fazer com que os sNaNs façam o que querem e explodam alguns programas!
Então, sem mais delongas:
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;
}
Compile, execute e obtenha o status de saída:
g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?
Resultado:
FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136
Observe que esse comportamento só acontece com o -O0
GCC 8.2: com -O3
, o GCC pré-calcula e otimiza todas as nossas operações sNaN! Não tenho certeza se existe uma maneira padrão compatível de evitar isso.
Portanto, deduzimos deste exemplo que:
snan + 1.0
causa FE_INVALID
, mas qnan + 1.0
não
O Linux só gera um sinal se estiver habilitado com feenableexept
.
Esta é uma extensão glibc, não consegui encontrar nenhuma maneira de fazer isso em nenhum padrão.
Quando o sinal acontece, é porque o próprio hardware da CPU levanta uma exceção, que o kernel do Linux manipula e informa a aplicação por meio do sinal.
O resultado é que o bash imprime Floating point exception (core dumped)
, e o status de saída é 136
, que corresponde ao sinal 136 - 128 == 8
, que de acordo com:
man 7 signal
é SIGFPE
.
Observe que SIGFPE
é o mesmo sinal que obteremos se tentarmos dividir um número inteiro por 0:
int main() {
int i = 1 / 0;
}
embora para inteiros:
- dividir qualquer coisa por zero aumenta o sinal, uma vez que não há representação infinita em inteiros
- o sinal acontece por padrão, sem a necessidade de
feenableexcept
Como manusear o SIGFPE?
Se você apenas criar um manipulador que retorna normalmente, isso leva a um loop infinito, porque depois que o manipulador retorna, a divisão acontece novamente! Isso pode ser verificado com GDB.
A única maneira é usar setjmp
e longjmp
pular para outro lugar, como mostrado em: C manipular o sinal SIGFPE e continuar a execução
Quais são algumas das aplicações do mundo real de sNaNs?
Sinceramente, ainda não entendi um caso de uso superútil para sNaNs, isso foi perguntado em: Utilidade da sinalização de NaN?
sNaNs parecem particularmente inúteis porque podemos detectar as operações inválidas iniciais ( 0.0f/0.0f
) que geram qNaNs com feenableexcept
: parece que snan
apenas levanta erros para mais operações que qnan
não levantam para, por exemplo ( qnan + 1.0f
).
Por exemplo:
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);
}
compilar:
gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm
então:
./main.out
dá:
Floating point exception (core dumped)
e:
./main.out 1
dá:
f1 -nan
f2 -nan
Veja também: Como rastrear um NaN em C ++
Quais são os sinalizadores e como são manipulados?
Tudo é implementado no hardware da CPU.
Os sinalizadores residem em algum registro, assim como o bit que diz se uma exceção / sinal deve ser levantada.
Esses registros são acessíveis a partir do userland da maioria dos archs.
Esta parte do código glibc 2.29 é realmente muito fácil de entender!
Por exemplo, fetestexcept
é implementado para x86_86 em 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)
portanto, vemos imediatamente que as instruções de uso stmxcsr
significam "Store MXCSR Register State".
E feenableexcept
é implementado em 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;
}
O que o padrão C diz sobre qNaN vs sNaN?
O rascunho do padrão C11 N1570 diz explicitamente que o padrão não diferencia entre eles em F.2.1 "Infinidades, zeros assinados e NaNs":
1 Esta especificação não define o comportamento da sinalização de NaNs. Geralmente usa o termo NaN para denotar NaNs silenciosos. As macros NAN e INFINITY e as funções nan <math.h>
fornecem designações para NaNs IEC 60559 e infinitos.
Testado em Ubuntu 18.10, GCC 8.2. Upstreams do GitHub: