web-dev-qa-db-fra.com

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

J'ai le code simple suivant:

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

speed1 et speed2 devrait 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 de lancer, mais j'aimerais comprendre pourquoi les valeurs sont différentes.

J'ai regardé le bytecode généré, mais à l'exception d'un magasin et d'une charge, 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 vrai code, 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.

127
Baalrukh

Tout d'abord, je suppose que vous savez que 6.2f * 10 n'est pas exactement 62 en raison de l'arrondi à virgule flottante (c'est en fait la valeur 61.99999809265137 lorsqu'elle est exprimée en double) et que votre question porte uniquement sur la raison pour laquelle deux calculs apparemment identiques entraînent une valeur incorrecte.

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

Dans le cas de float f = 6.2f * 10, vous prenez la valeur double 61,99999809265137 et arrondi au float le plus proche, qui est 62. Vous tronquez ensuite ce float en un 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 deuxième paramètre a une conversion implicite en float qui est mieux que la conversion implicite en double.

Le problème réel est que le compilateur est autorisé (mais pas obligatoire) à utiliser un intermédiaire qui est précision plus élevée que le type formel (section 11.2.2) . C'est pourquoi vous voyez un comportement différent sur différents systèmes: Dans l'expression (int)(6.2f * 10), le compilateur a la possibilité de conserver la valeur 6.2f * 10 sous une forme intermédiaire de haute précision avant la conversion en int. Si c'est le cas, le résultat est 61. Sinon, le résultat est 62.

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

167
Raymond Chen

La description

Les nombres flottants sont rarement exacts. 6.2f est quelque chose comme 6.1999998.... Si vous transformez ceci en un entier, il le tronquera et cela * 10 donnera 61.

Découvrez Jon Skeets DoubleConverter class. Avec cette classe, vous pouvez vraiment visualiser la valeur d'un nombre flottant sous forme de chaîne. Double et float sont tous deux des nombres flottants , la décimale ne l'est pas (c'est un nombre à virgule fixe).

Échantillon

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

Plus d'information

11
dknaack

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 au moment de la 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 ...

5
Thomas Levesque

J'ai compilé et désassemblé ce code (sur Win7/.NET 4.0). Je suppose que le compilateur évalue l'expression constante flottante comme double.

int speed1 = (int)(6.2f * 10);
   mov         dword ptr [rbp+8],3Dh       //result is precalculated (61)

float tmp = 6.2f * 10;
   movss       xmm0,dword ptr [000004E8h]  //precalculated (float format, xmm0=0x42780000 (62.0))
   movss       dword ptr [rbp+0Ch],xmm0 

int speed2 = (int)tmp;
   cvttss2si   eax,dword ptr [rbp+0Ch]     //instrunction converts float to Int32 (eax=62)
   mov         dword ptr [rbp+10h],eax 
1
Rodji

Je suppose que 6.2f la représentation réelle avec une précision flottante est 6.1999999 tandis que 62f est probablement quelque chose de similaire à 62.00000001. (int) casting toujours tronque la valeur décimale c'est pourquoi vous obtenez ce comportement.

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

1
InBetween

Single ne contient que 7 chiffres et lors de la conversion en Int32 le compilateur tronque tous les chiffres à 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 quand deux Single sont multipliés, dans ce cas il n'y a pas d'opération tronquée mais seulement une approximation.

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

0
Max Zerbini