131 votes

Expression flottante en C# : comportement étrange lors de la conversion du résultat flottant en int.

J'ai le code simple suivant :

int speed1 = (int)(6.2f * 10);
float tmp = 6.2f * 10;
int speed2 = (int)tmp;

speed1 et speed2 devraient avoir la même valeur, mais en fait, j'ai :

speed1 = 61
speed2 = 62

Je sais que je devrais probablement utiliser Math.Round au lieu du casting, mais j'aimerais comprendre pourquoi les valeurs sont différentes.

J'ai regardé le bytecode généré, mais à part un store et un load, les opcodes sont les mêmes.

J'ai également essayé le même code en java, et j'obtiens correctement 62 et 62.

Quelqu'un peut-il expliquer cela ?

Edit : Dans le code réel, ce n'est pas directement 6.2f * 10 mais un appel de fonction * une constante. J'ai le bytecode suivant :

pour speed1 :

IL_01b3:  ldloc.s    V_8
IL_01b5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ba:  ldc.r4     10.
IL_01bf:  mul
IL_01c0:  conv.i4
IL_01c1:  stloc.s    V_9

pour speed2 :

IL_01c3:  ldloc.s    V_8
IL_01c5:  callvirt   instance float32 myPackage.MyClass::getSpeed()
IL_01ca:  ldc.r4     10.
IL_01cf:  mul
IL_01d0:  stloc.s    V_10
IL_01d2:  ldloc.s    V_10
IL_01d4:  conv.i4
IL_01d5:  stloc.s    V_11

nous pouvons voir que les opérandes sont des flottants et que la seule différence est le stloc/ldloc .

Quant à la machine virtuelle, j'ai essayé avec Mono/Win7, Mono/MacOS et .NET/Windows, avec les mêmes résultats.

9 votes

Je pense que l'une des opérations a été effectuée en simple précision alors que l'autre a été effectuée en double précision. L'une d'entre elles a renvoyé une valeur légèrement inférieure à 62, ce qui donne 61 lors de la troncature en entier.

2 votes

Il s'agit de problèmes typiques de la précision des points flottants.

0 votes

Et pour faire bonne mesure (int)(6.2d * 10) retourne également 62 ce qui soutiendrait (en quelque sorte) ce que @Gabe a suggéré.

173voto

Raymond Chen Points 27887

Tout d'abord, je suppose que vous savez que 6.2f * 10 n'est pas exactement 62 en raison de l'arrondi en virgule flottante (il s'agit en fait de la valeur 61,99999809265137 lorsqu'elle est exprimée sous forme de double ) et que votre question porte uniquement sur la raison pour laquelle deux calculs apparemment identiques aboutissent à une valeur erronée.

La réponse est que dans le cas de (int)(6.2f * 10) vous prenez le double la valeur 61.99999809265137 et en la tronquant en un nombre entier, ce qui donne 61.

Dans le cas de float f = 6.2f * 10 vous prenez la double valeur 61.99999809265137 et arrondir au plus proche float qui est de 62. Vous tronquez ensuite cette valeur float en un nombre entier, et le résultat est 62.

Exercice : Expliquez les résultats de la séquence d'opérations suivante.

double d = 6.2f * 10;
int tmp2 = (int)d;
// evaluate tmp2

Mise à jour : Comme indiqué dans les commentaires, l'expression 6.2f * 10 est formellement un float puisque le second paramètre a une conversion implicite en float qui est meilleur que la conversion implicite en double .

Le problème actuel est que le compilateur est autorisé (mais pas obligé) à utiliser un intermédiaire qui est une précision supérieure à celle du type formel (section 11.2.2) . C'est pourquoi on observe des comportements différents selon les systèmes : Dans l'expression (int)(6.2f * 10) le compilateur a la possibilité de garder la valeur 6.2f * 10 sous une forme intermédiaire de haute précision avant d'être converti en int . Si c'est le cas, alors le résultat est 61. Si ce n'est pas le cas, le résultat est 62.

Dans le deuxième exemple, l'affectation explicite à float force l'arrondi à avoir lieu avant la conversion en nombre entier.

7 votes

Je ne suis pas sûr que cela réponde vraiment à la question. Pourquoi est-ce que (int)(6.2f * 10) en prenant le double valeur, comme f précise qu'il s'agit d'un float ? Je pense que le point principal (toujours sans réponse) est ici.

1 votes

Je pense que c'est le compilateur qui fait ça, puisque c'est float literal * int literal le compilateur a décidé qu'il était libre d'utiliser le meilleur type numérique, et pour gagner en précision il a opté pour double (peut-être). (cela expliquerait aussi que IL soit le même)

5 votes

Bon point. Le type de 6.2f * 10 est en fait float pas double . Je pense que le compilateur optimise l'intermédiaire, comme le permet le dernier paragraphe de la directive 11.1.6 .

12voto

dknaack Points 26873

Description

Les nombres flottants sont rarement exacts. 6.2f est quelque chose comme 6.1999998... . Si vous convertissez ce chiffre en un nombre entier, il sera tronqué et ce * 10 donne 61.

Allez voir Jon Skeets DoubleConverter classe. Avec cette classe, vous pouvez vraiment visualiser la valeur d'un nombre flottant sous forme de chaîne. Double et float sont tous deux nombres flottants mais pas les décimales (c'est un nombre à virgule fixe).

Echantillon

DoubleConverter.ToExactString((6.2f * 10))
// output 61.9999980926513671875

Plus d'informations

5voto

Thomas Levesque Points 141081

Regardez l'IL :

IL_0000:  ldc.i4.s    3D              // speed1 = 61
IL_0002:  stloc.0
IL_0003:  ldc.r4      00 00 78 42     // tmp = 62.0f
IL_0008:  stloc.1
IL_0009:  ldloc.1
IL_000A:  conv.i4
IL_000B:  stloc.2

Le compilateur réduit les expressions constantes du temps de compilation à leur valeur constante, et je pense qu'il fait une mauvaise approximation à un moment donné lorsqu'il convertit la constante en int . Dans le cas de speed2 cette conversion n'est pas faite par le compilateur, mais par le CLR, et ils semblent appliquer des règles différentes...

1voto

InBetween Points 6162

Je pense que 6.2f La représentation réelle avec une précision de type float est 6.1999999 tandis que 62f est probablement quelque chose de similaire à 62.00000001 . (int) Toujours le casting tronque la valeur décimale c'est pourquoi vous avez ce comportement.

EDIT : Selon les commentaires, j'ai reformulé le comportement des int casting à une définition beaucoup plus précise.

0 votes

Coulée vers un int tronque la valeur décimale, il n'arrondit pas.

0 votes

James D'Angelo : Désolé, l'anglais n'est pas ma langue maternelle. Je ne connaissais pas le mot exact, alors j'ai défini le comportement comme "arrondi vers le bas lorsqu'il s'agit de nombres positifs", ce qui décrit fondamentalement le même comportement. Mais oui, j'ai compris, tronqué est le mot exact pour cela.

0 votes

Pas de problème, c'est juste une symétrie mais ça peut causer des problèmes si quelqu'un commence à penser float -> int implique des arrondis. =D

0voto

Max Zerbini Points 1731

Single ne contient que 7 chiffres et lorsqu'il est transformé en un Int32 le compilateur tronque tous les chiffres en virgule flottante. Pendant la conversion, un ou plusieurs chiffres significatifs peuvent être perdus.

Int32 speed0 = (Int32)(6.2f * 100000000); 

donne le résultat de 619999980 donc (Int32)(6.2f * 10) donne 61.

C'est différent lorsque deux simples sont multipliés, dans ce cas il n'y a pas d'opération de troncature mais seulement une approximation.

Voir http://msdn.microsoft.com/en-us/library/system.single.aspx

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