Compilando o seguinte código:
double getDouble()
{
double value = 2147483649.0;
return value;
}
int main()
{
printf("INT_MAX: %u\n", INT_MAX);
printf("UINT_MAX: %u\n", UINT_MAX);
printf("Double value: %f\n", getDouble());
printf("Direct cast value: %u\n", (unsigned int) getDouble());
double d = getDouble();
printf("Indirect cast value: %u\n", (unsigned int) d);
return 0;
}
Saídas (MSVC x86):
INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649
Saídas (MSVC x64):
INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649
Na documentação da Microsoft, não há menção ao valor máximo de inteiro assinado em conversões de double
a unsigned int
.
Todos os valores acima INT_MAX
estão sendo truncados para 2147483648
quando for o retorno de uma função.
Estou usando o Visual Studio 2019 para construir o programa. Isso não acontece no gcc .
Estou fazendo algo errado? Existe uma maneira segura de converter double
para unsigned int
?
c
visual-c++
casting
x86
floating-point
Matheus Rossi Saciotto
fonte
fonte
INT_MIN
Respostas:
Um bug do compilador ...
Da montagem fornecida por @anastaciu, o código de elenco direto chama
__ftol2_sse
, o que parece converter o número para um longo assinado . O nome da rotina éftol2_sse
porque esta é uma máquina habilitada para sse - mas o float está em um registrador de ponto flutuante x87.; Line 17 call _getDouble call __ftol2_sse push eax push OFFSET ??_C@_0BH@GDLBDFEH@Direct?5cast?5value?3?5?$CFu?6@ call _printf add esp, 8
O elenco indireto, por outro lado,
; Line 18 call _getDouble fstp QWORD PTR _d$[ebp] ; Line 19 movsd xmm0, QWORD PTR _d$[ebp] call __dtoui3 push eax push OFFSET ??_C@_0BJ@HCKMOBHF@Indirect?5cast?5value?3?5?$CFu?6@ call _printf add esp, 8
que exibe e armazena o valor duplo para a variável local, em seguida, carrega-o em um registro SSE e chama
__dtoui3
que é uma rotina de conversão dupla para não assinada ...O comportamento do elenco direto não está em conformidade com C89; nem está de acordo com qualquer revisão posterior - mesmo C89 diz explicitamente que:
Eu acredito que o problema pode ser uma continuação disso desde 2005 - havia uma função de conversão chamada
__ftol2
que provavelmente teria funcionado para este código, ou seja, teria convertido o valor para um número sinalizado -2147483647, o que teria produzido o correto resultado quando interpretado como um número sem sinal.Infelizmente,
__ftol2_sse
não é um substituto imediato para__ftol2
, já que - em vez de apenas tomar os bits de valor menos significativo como estão - sinalizará o erro fora do intervalo retornandoLONG_MIN
/0x80000000
, que, interpretado como sem sinal há muito tempo, não é tudo o que era esperado. O comportamento de__ftol2_sse
seria válido parasigned long
, pois a conversão de um valor duplo a>LONG_MAX
parasigned long
teria comportamento indefinido.fonte
Seguindo a resposta de @ AnttiHaapala , testei o código usando otimização
/Ox
e descobri que isso removerá o bug, pois__ftol2_sse
não é mais usado://; 17 : printf("Direct cast value: %u\n", (unsigned int)getDouble()); push -2147483647 //; 80000001H push OFFSET $SG10116 call _printf //; 18 : double d = getDouble(); //; 19 : printf("Indirect cast value: %u\n", (unsigned int)d); push -2147483647 //; 80000001H push OFFSET $SG10117 call _printf add esp, 28 //; 0000001cH
As otimizações embutiram
getdouble()
e adicionaram avaliação de expressão constante, removendo assim a necessidade de uma conversão em tempo de execução, eliminando o bug.Só por curiosidade, fiz mais alguns testes, ou seja, alterando o código para forçar a conversão float para int no tempo de execução. Nesse caso o resultado ainda está correto, o compilador, com otimização, usa
__dtoui3
nas duas conversões://; 19 : printf("Direct cast value: %u\n", (unsigned int)getDouble(d)); movsd xmm0, QWORD PTR _d$[esp+24] add esp, 12 //; 0000000cH call __dtoui3 push eax push OFFSET $SG9261 call _printf //; 20 : double db = getDouble(d); //; 21 : printf("Indirect cast value: %u\n", (unsigned int)db); movsd xmm0, QWORD PTR _d$[esp+20] add esp, 8 call __dtoui3 push eax push OFFSET $SG9262 call _printf
No entanto, a prevenção do inlining
__declspec(noinline) double getDouble(){...}
trará o bug de volta://; 17 : printf("Direct cast value: %u\n", (unsigned int)getDouble(d)); movsd xmm0, QWORD PTR _d$[esp+76] add esp, 4 movsd QWORD PTR [esp], xmm0 call _getDouble call __ftol2_sse push eax push OFFSET $SG9261 call _printf //; 18 : double db = getDouble(d); movsd xmm0, QWORD PTR _d$[esp+80] add esp, 8 movsd QWORD PTR [esp], xmm0 call _getDouble //; 19 : printf("Indirect cast value: %u\n", (unsigned int)db); call __ftol2_sse push eax push OFFSET $SG9262 call _printf
__ftol2_sse
é chamado em ambas as conversões fazendo a saída2147483648
em ambas as situações, as suspeitas @zwol estavam corretas.Detalhes da compilação:
No Visual Studio:
Desativando
RTC
em Project->
Properties->
Code Generatione configuração de tempo de execução básico Cheques de default .Habilitando a otimização Project
->
Properties->
Optimizatione configurando a Otimização para / Ox .Com o depurador no
x86
modo.fonte
getDouble
fora da linha e / ou alterá-lo para retornar um valor que o compilador não pode provar ser constante.Ninguém olhou para o ASM para MS
__ftol2_sse
.A partir do resultado, podemos inferir que ele provavelmente foi convertido de x87 para assinado
int
/long
(ambos os tipos de 32 bits no Windows), em vez de com segurança parauint32_t
.x86 FP -> instruções de inteiro que estouram o resultado de inteiro não apenas quebram / truncam: elas produzem o que a Intel chama de "inteiro indefinido" quando o valor exato não é representável no destino: conjunto de bits alto, outros bits limpos. ie
0x80000000
.(Ou, se a exceção inválida de FP não for mascarada, ela será acionada e nenhum valor será armazenado. Mas no ambiente de FP padrão, todas as exceções de FP são mascaradas. É por isso que para cálculos de FP você pode obter um NaN em vez de uma falha.)
Isso inclui instruções x87 como
fistp
(usando o modo de arredondamento atual) e instruções SSE2 comocvttsd2si eax, xmm0
(usando truncamento em direção a 0, é isso quet
significa o extra ).Portanto, é um bug para compilar
double
->unsigned
conversão em uma chamada para__ftol2_sse
.Nota lateral / tangente:
Em x86-64, FP -> uint32_t pode ser compilado
cvttsd2si rax, xmm0
, convertendo para um destino assinado de 64 bits, produzindo o uint32_t que você deseja na metade inferior (EAX) do destino inteiro.É C e C ++ UB se o resultado estiver fora do intervalo 0..2 ^ 32-1, então está tudo bem que grandes valores positivos ou negativos deixarão a metade inferior de RAX (EAX) zero do padrão de bits indefinido inteiro. (Ao contrário das conversões de inteiros-> inteiros, a redução do módulo do valor não é garantida. O comportamento de converter um duplo negativo para um int sem sinal definido no padrão C? Comportamento diferente no ARM vs. x86 . Para ser claro, nada na questão é um comportamento indefinido ou mesmo definido pela implementação. Estou apenas apontando que se você tiver FP-> int64_t, poderá usá-lo para implementar FP-> uint32_t de forma eficiente. Isso inclui x87
fistp
que pode gravar um destino de inteiro de 64 bits mesmo no modo de 32 e 16 bits, ao contrário das instruções SSE2 que só podem lidar diretamente com inteiros de 64 bits no modo de 64 bits.fonte