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