Le double cast en int non signé sur Win32 est tronqué à 2 147 483 648

86

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 2147483648moment 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?

Matheus Rossi Saciotto
la source
24
Et non, vous ne faites rien de mal (peut-être en plus d'essayer d'utiliser le compilateur "C" de Microsoft)
Antti Haapala
5
Fonctionne sur ma machine ™, testé sur VS2017 v15.9.18 et VS2019 v16.4.1. Utilisez Aide> Envoyer des commentaires> Signaler un bogue pour leur parler de votre version.
Hans Passant le
5
Je suis capable de reproduire, j'ai les mêmes résultats que ceux de l'OP. VS2019 16.7.3.
anastaciu le
2
@EricPostpischil en effet, c'est le motif binaire deINT_MIN
Antti Haapala
6
Correction en attente
Antti Haapala le

Réponses:

71

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 est ftol2_ssedû 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 __dtoui3qui 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:

L'opération restante effectuée lorsqu'une valeur de type intégral est convertie en type non signé n'a pas besoin d'être effectuée lorsqu'une valeur de type flottant est convertie en type non signé. Ainsi, la plage de valeurs portables est [0, Utype_MAX + 1) .


Je pense que le problème pourrait être une continuation de cela à partir de 2005 - il y avait une fonction de conversion appelée __ftol2qui 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_ssen'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 renvoyant LONG_MIN/ 0x80000000, qui, interprété comme non signé ici n'est pas à tout ce qui était attendu. Le comportement de __ftol2_sseserait valide pour signed long, car la conversion d'un double d'une valeur> LONG_MAXen signed longaurait un comportement indéfini.

Antti Haapala
la source
23

Suite à la réponse de @ AnttiHaapala , j'ai testé le code en utilisant l'optimisation /Oxet j'ai trouvé que cela supprimera le bogue car il __ftol2_ssen'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 __dtoui3dans 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_sseest appelé dans les deux conversions faisant la sortie 2147483648dans les deux situations, les soupçons @zwol étaient corrects.


Détails de la compilation:

  • En utilisant la ligne de commande:
cl /permissive- /GS /analyze- /W3 /Gm- /Ox /sdl /D "WIN32" program.c        
  • Dans Visual Studio:

    • La désactivation RTCdans 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 x86mode.

anastaciu
la source
5
C'est drôle comme ils sont comme "ok avec les optimisations activées, le comportement indéfini sera vraiment indéfini" => le code fonctionne correctement: F
Antti Haapala
3
@AnttiHaapala, oui, oui, Microsoft à son meilleur.
anastaciu le
1
Les optimisations appliquées étaient l'évaluation de l'expression en ligne puis constante. Il ne fait plus de conversion float-int au moment de l'exécution. Je me demande si le bogue revient si vous forcez la getDoublesortie de ligne et / ou le modifiez pour renvoyer une valeur que le compilateur ne peut pas prouver est constante.
zwol le
1
@zwol, vous aviez raison, forcer la mise hors ligne et empêcher une évaluation constante ramènera le bogue, mais cette fois dans les deux conversions.
anastaciu le
7

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. ie0x80000000 .

(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 comme cvttsd2si eax, xmm0(en utilisant la troncature vers 0, c'est ce que tsignifie le supplément ).

C'est donc un bug à compiler double-> unsignedconversion 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 x87fistp 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.

Peter Cordes
la source
1
Je serais tenté de regarder dans ce code mais heureusement je n'ai pas MSVC ...: D
Antti Haapala
@AnttiHaapala: Ouais, moi non plus
Peter Cordes