Compiler le code suivant:
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;
}
Sorties (MSVC x86):
INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483648
Indirect cast value: 2147483649
Sorties (MSVC x64):
INT_MAX: 2147483647
UINT_MAX: 4294967295
Double value: 2147483649.000000
Direct cast value: 2147483649
Indirect cast value: 2147483649
Dans la documentation Microsoft, il n'est pas fait mention de la valeur maximale de l'entier signé dans les conversions de double
à unsigned int
.
Toutes les valeurs ci INT_MAX
- dessus sont tronquées au 2147483648
moment où il s'agit du retour d'une fonction.
J'utilise Visual Studio 2019 pour créer le programme. Cela ne se produit pas sur gcc .
Est-ce que je fais quelque chose de mal? Yat - il un moyen sûr de convertir double
à unsigned int
?
c
visual-c++
casting
x86
floating-point
Matheus Rossi Saciotto
la source
la source
INT_MIN
Réponses:
Un bogue du compilateur ...
À partir de l'assembly fourni par @anastaciu, le code de distribution directe appelle
__ftol2_sse
, ce qui semble convertir le nombre en un long signé . Le nom de la routine estftol2_sse
dû au fait qu'il s'agit d'une machine compatible sse - mais le float est dans un registre à virgule flottante 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
Le casting indirect, d'autre part, fait
; 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
qui saute et stocke la valeur double dans la variable locale, puis la charge dans un registre SSE et appelle
__dtoui3
qui est une routine de conversion double en int unsigned ...Le comportement de la distribution directe n'est pas conforme à C89; il n'est pas non plus conforme à une révision ultérieure - même C89 dit explicitement que:
Je pense que le problème pourrait être une continuation de cela à partir de 2005 - il y avait une fonction de conversion appelée
__ftol2
qui aurait probablement fonctionné pour ce code, c'est-à-dire qu'elle aurait converti la valeur en un nombre signé -2147483647, ce qui aurait produit le bon résultat lors de l'interprétation d'un nombre non signé.Malheureusement, ce
__ftol2_sse
n'est pas un remplacement instantané pour__ftol2
, comme il le ferait - au lieu de simplement prendre les bits de valeur les moins significatifs tels quels - signaler l'erreur hors plage en renvoyantLONG_MIN
/0x80000000
, qui, interprété comme non signé ici n'est pas à tout ce qui était attendu. Le comportement de__ftol2_sse
serait valide poursigned long
, car la conversion d'un double d'une valeur>LONG_MAX
ensigned long
aurait un comportement indéfini.la source
Suite à la réponse de @ AnttiHaapala , j'ai testé le code en utilisant l'optimisation
/Ox
et j'ai trouvé que cela supprimera le bogue car il__ftol2_sse
n'est plus utilisé://; 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
Les optimisations ont intégré
getdouble()
et ajouté une évaluation d'expression constante, supprimant ainsi le besoin d'une conversion au moment de l'exécution, faisant disparaître le bogue.Juste par curiosité, j'ai fait quelques tests supplémentaires, à savoir changer le code pour forcer la conversion float-int à l'exécution. Dans ce cas, le résultat est toujours correct, le compilateur, avec optimisation, utilise
__dtoui3
dans les deux conversions://; 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
Cependant, empêcher l'inlining
__declspec(noinline) double getDouble(){...}
ramènera le bogue://; 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
est appelé dans les deux conversions faisant la sortie2147483648
dans les deux situations, les soupçons @zwol étaient corrects.Détails de la compilation:
Dans Visual Studio:
La désactivation
RTC
dans Project->
Properties->
Code Generationet réglage des vérifications de base d' exécution à défaut .Activation de l'optimisation Project
->
Properties->
Optimizationet réglage de l' optimisation sur / Ox .Avec le débogueur en
x86
mode.la source
getDouble
sortie de ligne et / ou le modifiez pour renvoyer une valeur que le compilateur ne peut pas prouver est constante.Personne n'a examiné l'asm pour MS
__ftol2_sse
.À partir du résultat, nous pouvons déduire qu'il a probablement converti de x87 en signé
int
/long
(les deux types 32 bits sous Windows), au lieu de le convertir en toute sécuritéuint32_t
.x86 FP -> les instructions entières qui débordent le résultat entier ne se contentent pas de wrapper / tronquer: elles produisent ce qu'Intel appelle «entier indéfini» lorsque la valeur exacte n'est pas représentable dans la destination: jeu de bits haut, autres bits effacés. ie
0x80000000
.(Ou si l'exception non valide FP n'est pas masquée, elle se déclenche et aucune valeur n'est stockée. Mais dans l'environnement FP par défaut, toutes les exceptions FP sont masquées. C'est pourquoi, pour les calculs FP, vous pouvez obtenir un NaN au lieu d'une erreur.)
Cela inclut à la fois les instructions x87 comme
fistp
(en utilisant le mode d'arrondi actuel) et les instructions SSE2 commecvttsd2si eax, xmm0
(en utilisant la troncature vers 0, c'est ce quet
signifie le supplément ).C'est donc un bug à compiler
double
->unsigned
conversion en un appel à__ftol2_sse
.Note latérale / tangente:
Sur x86-64, FP -> uint32_t peut être compilé vers
cvttsd2si rax, xmm0
, convertissant en une destination signée 64 bits, produisant le uint32_t que vous voulez dans la moitié inférieure (EAX) de la destination entière.C'est C et C ++ UB si le résultat est en dehors de la plage 0..2 ^ 32-1, il est donc normal que d'énormes valeurs positives ou négatives laissent la moitié inférieure de RAX (EAX) zéro à partir du motif de bits entier indéfini. (Contrairement aux conversions entiers -> entiers, la réduction modulo de la valeur n'est pas garantie. Le comportement de conversion d'un double négatif en un entier non signé est-il défini dans le standard C? Comportement différent sur ARM par rapport à x86 . Pour être clair, rien dans la question est un comportement indéfini ou même défini par l'implémentation. Je souligne simplement que si vous avez FP-> int64_t, vous pouvez l'utiliser pour implémenter efficacement FP-> uint32_t. Cela inclut x87
fistp
qui peut écrire une destination d'entiers 64 bits même en mode 32 bits et 16 bits, contrairement aux instructions SSE2 qui ne peuvent traiter directement que des entiers 64 bits en mode 64 bits.la source