changement de signe lors du passage de l’int à flot et retour

Considérez le code suivant, qui est un SSCCE de mon problème actuel:

#include  int roundsortingp(int x) { return int(float(x)); } int main() { int a = 2147483583; int b = 2147483584; std::cout << a < " << roundtrip(a) << '\n'; std::cout << b < " << roundtrip(b) << '\n'; } 

La sortie sur mon ordinateur (Xubuntu 12.04.3 LTS) est la suivante:

 2147483583 -> 2147483520 2147483584 -> -2147483648 

Notez que le nombre positif b devient négatif après l’aller-retour. Ce comportement est-il bien spécifié? Je m’attendrais à ce que le décollage int-to-float préserve au moins correctement le signe …

Hm, sur ideone , la sortie est différente:

 2147483583 -> 2147483520 2147483584 -> 2147483647 

L’équipe g ++ a-t-elle corrigé un bogue entre-temps ou les deux sorties sont-elles parfaitement valides?

Votre programme appelle un comportement indéfini en raison d’un débordement dans la conversion d’un nombre à virgule flottante en entier. Ce que vous voyez n’est que le symptôme habituel sur les processeurs x86.

La valeur float la plus proche de 2147483584 est 2 31 exactement (la conversion de l’entier en virgule flottante arrondit généralement au plus proche, ce qui peut être supérieur et est en hausse dans ce cas. Pour être précis, le comportement lors de la conversion d’un entier en flottant). point est défini par l’implémentation, la plupart des implémentations définissent l’arrondi comme étant «selon le mode d’arrondi de la FPU», et le mode d’arrondi par défaut de la FPU est arrondi au plus proche).

Ensuite, lors de la conversion du flottant représentant 2 31 en int , un débordement se produit. Ce débordement est un comportement indéfini. Certains processeurs soulèvent une exception, d’autres saturent. L’instruction IA-32 cvttsd2si généralement générée par les compilateurs renvoie toujours INT_MIN en cas de dépassement, que le flottant soit positif ou négatif.

Vous ne devez pas vous fier à ce comportement même si vous savez que vous ciblez un processeur Intel: lors du ciblage de x86-64, les compilateurs peuvent émettre, pour la conversion d’un nombre à virgule flottante en entier, des séquences d’instructions exploitant le comportement indéfini. résultats autres que ce que vous pourriez normalement attendre pour le type entier de destination .

La réponse de Pascal est correcte – mais manque de détails, ce qui implique que certains utilisateurs ne le comprennent pas ;-). Si vous êtes intéressé par son apparence au niveau inférieur (en supposant que le coprocesseur et non le logiciel gère les opérations en virgule flottante) – lisez la suite.

Dans 32 bits de float (IEEE 754), vous pouvez stocker tous les entiers compris dans la plage [-2 24 … 2 24 ] . Les entiers en dehors de la plage peuvent également avoir une représentation exacte en tant que valeur flottante, mais pas tous. Le problème est que vous ne pouvez avoir que 24 bits significatifs avec lesquels vous pouvez jouer en mode float.

Voici comment la conversion depuis int-> float ressemble généralement à un niveau bas:

 fild dword ptr[your int] fstp dword ptr[your float] 

C’est juste la séquence de 2 instructions de coprocesseur. Il charge d’abord 32 bits int sur la stack du comprocessor et le convertit en flotteur de 80 bits.

Manuel du développeur du logiciel Intel® 64 et IA-32 Architectures

(PROGRAMMATION AVEC LE X87 FPU):

Lorsque des valeurs entières BCD à virgule flottante, à nombre entier ou empaqueté sont chargées depuis la mémoire dans l’un des registres de données FPU x87, les valeurs sont automatiquement converties au format à virgule flottante double précision étendue (si elles ne le sont pas déjà).

Comme les registres FPU sont des flotteurs de 80 bits de large – il n’y a pas de problème avec fild ici car 32 bits int s’adapte parfaitement au significande 64 bits du format à virgule flottante.

Jusqu’ici tout va bien.

La deuxième partie – fstp est un peu délicate et peut être surprenante. Il est supposé stocker des virgules flottantes de 80 bits en 32 bits. Bien qu’il s’agisse de valeurs entières (dans la question), le coprocesseur peut effectivement effectuer un «arrondi». Ke? Comment arrondissez-vous la valeur entière même si elle est stockée en format virgule flottante? ;-).

Je vais l’expliquer brièvement – voyons d’abord quels sont les modes d’arrondi x87 (ils sont l’incarnation des modes d’arrondi IEE 754). Le fpu X87 dispose de 4 modes d’arrondi contrôlés par les bits n ° 10 et n ° 11 du mot de contrôle de fpu:

  • 00 – au pair le plus proche – Le résultat arrondi est le plus proche du résultat infiniment précis. Si deux valeurs sont également proches, le résultat est la valeur paire (c’est-à-dire celle avec le bit le moins significatif de zéro). Défaut
  • 01 – vers -Inf
  • 10 – vers + inf
  • 11 – vers 0 (c’est-à-dire tronqué)

Vous pouvez jouer avec les modes d’arrondi en utilisant ce code simple (bien que cela puisse être fait différemment – en affichant un niveau bas ici):

 enum ROUNDING_MODE { RM_TO_NEAREST = 0x00, RM_TOWARD_MINF = 0x01, RM_TOWARD_PINF = 0x02, RM_TOWARD_ZERO = 0x03 // TRUNCATE }; void set_round_mode(enum ROUNDING_MODE rm) { short csw; short tmp = rm; _asm { push ax fstcw [csw] mov ax, [csw] and ax, ~(3<<10) shl [tmp], 10 or ax, tmp mov [csw], ax fldcw [csw] pop ax } } 

Ok gentil mais comment cela est-il lié aux valeurs entières? Patience ... pour comprendre pourquoi des modes d'arrondi peuvent être nécessaires dans int pour faire flotter la vérification de la manière la plus évidente de convertir int en float - truncation (pas par défaut) - cela peut ressembler à ceci:

  • signe d'enregistrement
  • nier votre int si inférieur à zéro
  • trouver la position de gauche 1
  • décalage int vers la droite / gauche pour que 1 trouvé ci-dessus soit positionné sur le bit n ° 23
  • enregistrer le nombre de décalages au cours du processus afin de pouvoir calculer l'exposant

Et le code simulant ce comportement peut ressembler à ceci:

 float int2float(int value) { // handles all values from [-2^24...2^24] // outside this range only some integers may be represented exactly // this method will use truncation 'rounding mode' during conversion // we can safely reinterpret it as 0.0 if (value == 0) return 0.0; if (value == (1U<<31)) // ie -2^31 { // -(-2^31) = -2^31 so we'll not be able to handle it below - use const value = 0xCF000000; return *((float*)&value); } int sign = 0; // handle negative values if (value < 0) { sign = 1U << 31; value = -value; } // although right shift of signed is undefined - all compilers (that I know) do // arithmetic shift (copies sign into MSB) is what I prefer here // hence using unsigned abs_value_copy for shift unsigned int abs_value_copy = value; // find leading one int bit_num = 31; int shift_count = 0; for(; bit_num > 0; bit_num--) { if (abs_value_copy & (1U<= 23) { // need to shift right shift_count = bit_num - 23; abs_value_copy >>= shift_count; } else { // need to shift left shift_count = 23 - bit_num; abs_value_copy <<= shift_count; } break; } } // exponent is biased by 127 int exp = bit_num + 127; // clear leading 1 (bit #23) (it will implicitly be there but not stored) int coeff = abs_value_copy & ~(1<<23); // move exp to the right place exp <<= 23; int ret = sign | exp | coeff; return *((float*)&ret); } 

Maintenant, exemple - le mode de troncature convertit 2147483583 à 2147483520 .

 2147483583 = 01111111_11111111_11111111_10111111 

Pendant la conversion int-> float, vous devez déplacer le bit le plus à gauche 1 vers le bit # 23. Maintenant, le premier est dans le bit # 30. Pour le placer dans le bit n ° 23, vous devez effectuer un décalage droit de 7 positions. Pendant ce temps, vous perdez (ils ne rentreront pas dans le format flottant 32 bits) 7 bits lsb à partir de la droite (vous tronquez / coupez). Ils étaient:

 01111111 = 63 

Et 63 est quel nombre original perdu:

 2147483583 -> 2147483520 + 63 

Truncating est facile mais peut ne pas être nécessairement ce que vous voulez et / ou est le meilleur pour tous les cas. Prenons l'exemple ci-dessous:

 67108871 = 00000100_00000000_00000000_00000111 

La valeur ci-dessus ne peut pas être exactement représentée par un flottant, mais vérifiez ce que la troncature lui fait. Comme précédemment, nous devons déplacer le bit le plus à gauche 1 au bit 23. Cela nécessite que la valeur soit décalée exactement de 3 positions en perdant 3 bits LSB (à partir de maintenant, j'écrirai des nombres différemment, montrant où le 24ème bit de flottant est implicite et mettra entre parenthèses explicites 23b).

 00000001.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out) 

Troncature hache 3 bits de fin nous laissant avec 67108864 (67108864 + 7 (3 bits hachés)) = 67108871 (rappelez-vous bien que nous décalons nous compensons avec la manipulation des exposants - omis ici).

Est-ce suffisant? Hey 67108872 est parfaitement représentable par 32bit float et devrait être beaucoup mieux que 67108864 non? CORRECT et c'est là que vous voudrez peut-être parler d’arrondi lors de la conversion de l’int en 32 bits.

Voyons maintenant comment fonctionne le mode par défaut "arrondi au plus proche" et quelles sont ses implications dans le cas de OP. Considérons le même exemple une fois de plus.

 67108871 = 00000100_00000000_00000000_00000111 

Comme nous le soaps, nous avons besoin de 3 bons changements pour placer le plus à gauche 1 dans le bit n ° 23:

 00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out) 

La procédure consistant à «arrondir au nombre le plus proche» implique la recherche de 2 nombres entre la valeur d'entrée 67108871 et la valeur la plus proche possible. Gardez à l'esprit que nous fonctionnons toujours dans FPU sur 80 bits, bien que je montre certains bits décalés, ils sont toujours dans FPU reg mais seront supprimés lors de l'arrondissement lors du stockage de la valeur de sortie.

 00000000_1.[0000000_00000000_00000000] 111 * 2^26 (3 bits shifted out) 

2 valeurs proches du support 00000000_1.[0000000_00000000_00000000] 111 * 2^26 sont:

de haut:

  00000000_1.[0000000_00000000_00000000] 111 * 2^26 +1 = 00000000_1.[0000000_00000000_00000001] * 2^26 = 67108872 

et d'en bas:

  00000000_1.[0000000_00000000_00000000] * 2^26 = 67108864 

Il est évident que 67108872 est beaucoup plus proche de 67108871 que de 67108864 conséquent, la conversion à partir de la valeur int de 32 bits 67108871 donne 67108872 (en arrondissant au mode pair le plus proche).

Maintenant, les numéros d'OP (toujours arrondis au plus proche):

  2147483583 = 01111111_11111111_11111111_10111111 = 00000000_1.[1111111_11111111_11111111] 0111111 * 2^30 

valeurs de parenthèses:

Haut:

  00000000_1.[1111111_111111111_11111111] 0111111 * 2^30 +1 = 00000000_10.[0000000_00000000_00000000] * 2^30 = 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648 

bas:

 00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520 

Gardez à l’esprit que même un mot dans «Arrondir au plus proche pair» n’est important que lorsque la valeur d’entrée est à mi-chemin entre les valeurs entre crochets. Ce n'est qu'alors que le mot compte et «décide» quelle valeur de parenthèse doit être sélectionnée. Dans le cas ci-dessus, même n'a pas d'importance et nous devons simplement choisir une valeur plus proche, qui est 2147483520

Le cas du dernier OP montre le problème où même le mot importe. :

  2147483584 = 01111111_11111111_11111111_11000000 = 00000000_1.[1111111_11111111_11111111] 1000000 * 2^30 

les valeurs de parenthèses sont les mêmes que précédemment:

top: 00000000_1.[0000000_00000000_00000000] * 2^31 = 2147483648

en bas: 00000000_1.[1111111_111111111_11111111] * 2^30 = 2147483520

Il n'y a pas de valeur plus proche maintenant (2147483648-2147483584 = 64 = 2147483584-2147483520), nous devons donc compter sur pair et sélectionner la valeur top (pair) 2147483648 .

Et ici, le problème de OP est que Pascal avait brièvement décrit. FPU ne fonctionne que sur les valeurs signées et 2147483648 ne peut pas être enregistré comme signé int car sa valeur maximale est 2147483647, d'où des problèmes.

Preuve simple (sans citations de documentation) que FPU ne fonctionne que sur des valeurs signées, c.-à-d. traite chaque valeur comme signée en déboguant ceci:

 unsigned int test = (1u << 31); _asm { fild [test] } 

Bien qu'il semble que la valeur de test soit considérée comme non signée, elle sera chargée à -2 31 car il n'y a pas d'instructions séparées pour charger les valeurs signées et non signées dans FPU. De même, vous ne trouverez pas d'instructions vous permettant de stocker la valeur non signée de FPU à mem. Tout n'est qu'un modèle de bit traité comme signé, quelle que soit la manière dont vous l'avez déclaré dans votre programme.

Était long mais j'espère que quelqu'un va apprendre quelque chose.