42 votes

Les signes changent lorsqu'on passe de int à float puis de float à int

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

#include 

int roundtrip(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 :

2147483583 -> 2147483520
2147483584 -> -2147483648

Remarquez comment le nombre positif b devient négatif après le roundtrip. Ce comportement est-il bien spécifié ? J'aurais attendu que le round-tripping int-to-float conserve au moins correctement le signe...

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

2147483583 -> 2147483520
2147483584 -> 2147483647

Est-ce que l'équipe de g++ a corrigé un bug entre-temps, ou les deux sorties sont-elles parfaitement valides ?

68voto

Pascal Cuoq Points 39606

Votre programme provoque un comportement indéfini en raison d'un débordement lors de la conversion de 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 exactement 231 (la conversion d'entier en virgule flottante arrondit généralement au plus proche, ce qui peut être à la hausse, et c'est le cas ici. Pour être précis, le comportement lors de la conversion d'entier en virgule flottante est défini par l'implémentation, la plupart des implémentations définissent l'arrondi comme étant "selon le mode d'arrondi du FPU", et le mode d'arrondi par défaut du FPU arrondit au plus proche).

Ensuite, lors de la conversion du flottant représentant 231 en int, un dépassement se produit. Ce dépassement est un comportement indéfini. Certains processeurs déclenchent une exception, d'autres saturant. L'instruction IA-32 cvttsd2si généralement générée par les compilateurs se trouve toujours à renvoyer INT_MIN en cas de dépassement, que le flottant soit positif ou négatif.

Vous ne devriez pas vous fier à ce comportement même si vous savez que vous ciblez un processeur Intel : en ciblant x86-64, les compilateurs peuvent émettre, pour la conversion de virgule flottante en entier, des séquences d'instructions qui exploitent le comportement indéfini pour renvoyer des résultats autres que ce que vous pourriez attendre pour le type entier de destination.

10voto

Artur Points 3152

Pascal réponse est OK - mais manque de détails qui implique que certains utilisateurs n'ont pas l'obtenir ;-) . Si vous êtes intéressé par la façon dont il regarde au niveau inférieur (en supposant que le coprocesseur et pas logiciel gère les opérations à virgule flottante) - lisez la suite.

En 32 bits float (IEEE 754) - vous pouvez stocker tous les entiers de l'intérieur [-224...224] gamme. Entiers en dehors de la plage peuvent également avoir une représentation exacte en tant que float mais tous n'ont pas. Le problème est que vous ne pouvez avoir qu'24 bits significatifs pour jouer avec dans le flotteur.

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

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

C'est juste séquence de 2 instructions de coprocesseur. Premier chargement de 32 bits int sur comprocessor la pile et le convertit en 80 bits float.

Intel® 64 et IA-32 Architectures Manuel de développement Logiciel

(PROGRAMMATION AVEC LA FPU X87):

Quand à virgule flottante, entier ou emballés BCD entier les valeurs sont chargés à partir de la mémoire dans toute la FPU x87 registres de données, les valeurs sont automatiquement converti en double étendu à virgule flottante au format (si ils ne sont pas déjà dans ce format).

Depuis les registres FPU sont 80bit large flotte - il n'y a pas de problème avec l' fild ici qu'en 32bit int parfaitement dans 64bit significande de format à virgule flottante.

So far So good.

La deuxième partie - fstp est un peu délicat et peut-être surprenant. Il est censé stocker 80bit à virgule flottante de 32 bits float. Bien qu'il est tout au sujet des valeurs entières (dans la question) coprocesseur peut effectuer réellement 'arrondi'. Ke? Comment faites-vous faire un tour entier de la valeur même si elle est stockée en format à virgule flottante? ;-).

Je vais vous expliquer en peu de temps - nous allons tout d'abord voir ce que les modes d'arrondi x87 offre (ils sont IEE 754 modes d'arrondi' incarnation). Fpu X87 dispose de 4 modes d'arrondi contrôlé par les bits #10 et #11 de la fpu du mot de contrôle:

  • 00 - le plus proche même résultat Arrondi est le plus proche de l'infiniment résultat précis. Si deux les valeurs sont proches de la même façon, le résultat est le même valeur (qui est, le celui avec le moins de bits significatifs de zéro). Par défaut
  • 01 - vers -Inf
  • 10 - vers +inf
  • 11 - vers 0 (c'est à dire. tronquer)

Vous pouvez jouer avec les modes d'arrondi à l'aide de ce code simple (bien qu'il puisse être fait différemment - faible niveau 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 sympa, mais encore, comment est-ce lié à des valeurs entières? De la Patience ... pour comprendre pourquoi vous pourriez avoir besoin d'arrondi les modes impliqués dans int float conversion de vérifier le plus évident de la conversion de int en float - la troncature (pas par défaut) - qui peut ressembler à ceci:

  • enregistrement signe
  • annuler votre int si elle est inférieure à zéro
  • trouver une position de gauche 1
  • maj int vers la droite/gauche, de sorte que 1 se trouve au-dessus est placé sur le bit #23
  • le record du nombre de quarts de travail pendant le processus de sorte que vous pouvez calculer l'exposant

Et le code de simuler 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<<bit_num))
        {
            if (bit_num >= 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 troncature mode convertit 2147483583 de 2147483520.

2147483583 = 01111111_11111111_11111111_10111111

Au cours de int->float conversion vous devez maj gauche 1 bit n ° 23. Aujourd'hui à la tête 1 est le bit n ° 30. Afin de le placer dans le bit n ° 23, vous devez effectuer la maj de droite en 7 positions. Pendant que vous perdez (ils ne rentre pas dans le format flottant 32 bits) 7 lsb bits à partir de la droite (vous tronquez/chop). Ils ont été:

01111111 = 63

Et 63 est ce numéro original perdu:

2147483583 -> 2147483520 + 63

La troncation est facile mais pas forcément ce que vous voulez et/ou qui est le meilleur pour tous les cas. Considérons l'exemple ci-dessous:

67108871 = 00000100_00000000_00000000_00000111

Au-dessus de la valeur ne peut pas être exactement représentée par flotteur, mais vérifiez ce que la troncature fait pour elle. Comme précédemment, nous devons changer de gauche 1 bit n ° 23. Cela nécessite de la valeur à être décalé à droite exactement 3 positions de perdre 3 bits LSB (à compter de maintenant, je vais écrire les nombres différemment montrant où implicite 24 bits float est et sera support explicite 23bits de significande):

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

La troncature des côtelettes de 3 de fuite bits nous laissant avec 67108864 (67108864+7(3 haché bits)) = 67108871 (rappelez-vous bien que nous passons nous de compenser avec l'exposant de manipulation - omis ici).

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

Maintenant, nous allons voir comment défaut "d'arrondi au plus près, même en mode" fonctionne et quelles sont ses implications dans des OP cas. Considérons le même exemple une fois de plus.

67108871 = 00000100_00000000_00000000_00000111

Comme nous le savons, nous avons besoin de 3 droit des variations à la place la plus à gauche 1 dans le bit n ° 23:

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

La procédure de "l'arrondi au plus proche même de" consiste à trouver 2 numéros que support de la valeur d'entrée 67108871 par le bas et au-dessus, aussi près que possible. Gardez à l'esprit que nous avons toujours fonctionner à l'intérieur de la FPU sur 80bits tellement bien que je montre quelques bits décalés qu'ils sont encore dans la FPU, reg, mais seront supprimées lors de l'opération d'arrondi lors du stockage de la valeur de sortie.

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

2 valeurs qui sont étroitement support 00000000_1.[0000000_00000000_00000000] 111 * 2^26 sont:

de haut en bas:

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

et à partir de ci-dessous:

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

Évidemment 67108872 est beaucoup plus proche d' 67108871 que 67108864 ainsi la conversion de 32 bits int valeur 67108871 donne 67108872 (en arrondi au plus proche, même mode).

Maintenant, OP numéros (toujours arrondi au plus proche même):

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

support de valeurs:

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

en bas:

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

Gardez à l'esprit que même mot dans 'l'arrondi au plus proche, même à des" questions uniquement lorsque la valeur d'entrée est à mi-chemin entre le support de valeurs. Alors seulement, le mot même les questions et "décide" qui support la valeur doit être sélectionnée. Dans le cas ci-dessus , même n'a pas d'importance et nous devons simplement choisir la plus proche de la valeur, qui est - 2147483520

Dernière OP cas, montre que le problème où le même mot compte. :

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

support de valeurs sont les mêmes que précédemment:

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

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

Il n'y a pas plus proche de la valeur maintenant (2147483648-2147483584=64=2147483584-2147483520) donc, nous devons compter sur la même et sélectionnez haut (même) valeur 2147483648.

Et ici, OP problème, c'est que Pascal a brièvement décrit. FPU ne fonctionne que sur des valeurs signées et 2147483648 ne peut pas être stockée comme signé int comme sa valeur max est de 2 147 483 647 par conséquent, des questions.

Preuve Simple (sans documentation guillemets) que FPU ne fonctionne que sur des valeurs signées ie. traite chaque valeur est signé par le débogage ceci:

unsigned int test = (1u << 31);

_asm
{
    fild [test]
}

Bien qu'il ressemble à valeur de test doivent être traités comme des non signé, il sera chargé comme -231 comme il n'y a pas d'instructions pour le chargement des entiers signés et non signés valeurs dans la FPU. De même, vous n'allez pas trouver des instructions qui vous permettra de stocker de valeur non signée de la FPU, mem. Tout est une séquence de bits traités comme signé indépendamment de la façon dont vous pourriez avoir déclaré dans votre programme.

Été long mais j'espère que quelqu'un va apprendre quelque chose hors de lui.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X