Expression C # Float: comportement étrange lors de la conversion du résultat float en int

128

J'ai le code simple suivant:

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

speed1et speed2devrait 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 cast, mais j'aimerais comprendre pourquoi les valeurs sont différentes.

J'ai regardé le bytecode généré, mais à part un magasin et 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.

Pour ce qui est de la machine virtuelle, j'ai essayé avec Mono / Win7, Mono / MacOS et .NET / Windows, avec les mêmes résultats.

Baalrukh
la source
9
Je suppose que l'une des opérations a été effectuée en simple précision tandis que l'autre a été effectuée en double précision. L'un d'eux a renvoyé une valeur légèrement inférieure à 62, ce qui donne 61 lors de la troncature en un entier.
Gabe
2
Ce sont des problèmes typiques de précision en virgule flottante.
TJHeuvel
3
Essayer ceci sur .Net / WinXP, .Net / Win7, Mono / Ubuntu et Mono / OSX donne vos résultats pour les deux versions de Windows, mais 62 pour speed1 et speed2 dans les deux versions Mono. Merci @BoltClock
Eugen Rieck
6
M. Lippert ... vous dans les parages ??
vc 74
6
L'évaluateur d'expression constante du compilateur ne gagne aucun prix ici. Clairement, il tronque 6.2f dans la première expression, il n'a pas de représentation exacte en base 2 et finit donc par 6.199999. Mais ne le fait pas dans la 2ème expression, probablement en réussissant à la garder en double précision d'une manière ou d'une autre. C'est autrement normal pour le cours, la cohérence en virgule flottante n'est jamais un problème. Cela ne va pas être résolu, vous connaissez la solution de contournement.
Hans Passant

Réponses:

168

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

La réponse est que dans le cas de (int)(6.2f * 10), vous prenez la doublevaleur 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 arrondissez au plus proche float, qui est 62. Vous tronquez ensuite cela floatà 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 * 10est formellement a floatpuisque le deuxième paramètre a une conversion implicite vers floatlaquelle est meilleure 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 plus précis 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 * 10sous une forme intermédiaire de haute précision avant la conversion en int. Si tel est le cas, 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 à floatforce l'arrondi avant la conversion en entier.

Raymond Chen
la source
6
Je ne suis pas sûr que cela réponde réellement à la question. Pourquoi (int)(6.2f * 10)prend la doublevaleur, comme fspécifie que c'est un float? Je pense que le point principal (toujours sans réponse) est ici.
ken2k
1
Je pense que c'est le compilateur qui fait cela, puisqu'il s'agit d'un littéral float * int littéral, le compilateur a décidé qu'il était libre d'utiliser le meilleur type numérique, et pour économiser la précision, il est parti pour le double (peut-être). (expliquerait également IL étant le même)
George Duckett
5
Bon point. Le type de 6.2f * 10est en fait float, non double. Je pense que le compilateur optimise l'intermédiaire, comme le permet le dernier paragraphe de 11.1.6 .
Raymond Chen
3
Il a la même valeur (la valeur est 61,99999809265137). La différence est le chemin emprunté par la valeur pour devenir un entier. Dans un cas, il passe directement à un entier, et dans un autre, il passe d'abord par une floatconversion.
Raymond Chen
38
La réponse de Raymond ici est bien sûr tout à fait correcte. Je note que le compilateur C # et le compilateur jit sont tous deux autorisés à utiliser plus de précision à tout moment , et de le faire de manière incohérente . Et en fait, c'est exactement ce qu'ils font. Cette question a été soulevée des dizaines de fois sur StackOverflow; voir stackoverflow.com/questions/8795550/… pour un exemple récent.
Eric Lippert
11

La description

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

Découvrez la DoubleConverterclasse Jon Skeets . Avec cette classe, vous pouvez vraiment visualiser la valeur d'un nombre flottant sous forme de chaîne. Doubleet floatsont tous deux des nombres flottants , décimal ne l'est pas (c'est un nombre à virgule fixe).

Échantillon

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

Plus d'information

dknaack
la source
5

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 effectuée par le compilateur, mais par le CLR, et ils semblent appliquer des règles différentes ...

Thomas Levesque
la source
1

Je pense que la 6.2freprésentation réelle avec une précision de flotteur est 6.1999999en 62fest probablement quelque chose de similaire à 62.00000001. (int)le cast tronque toujours la valeur décimale , c'est pourquoi vous obtenez ce comportement.

EDIT : Selon les commentaires, j'ai reformulé le comportement du intcasting pour une définition beaucoup plus précise.

Entre
la source
La conversion en un inttronque la valeur décimale, elle n'est pas arrondie.
Jim D'Angelo
@James D'Angelo: Désolé, l'anglais n'est pas ma langue principale. Je ne connaissais pas le mot exact, j'ai donc défini le comportement comme «arrondi vers le bas lorsqu'il s'agit de nombres positifs», ce qui décrit essentiellement le même comportement. Mais oui, point pris, tronquer est le mot exact pour cela.
Entre le
pas de problème, ce n'est que symantique mais peut causer des problèmes si quelqu'un commence à réfléchir float-> intimplique l'arrondi. = D
Jim D'Angelo
1

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 
Rodji
la source
0

Singlene contient que 7 chiffres et lors de sa conversion en un, Int32le compilateur tronque tous les chiffres à virgule flottante. Lors de 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 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

Massimo Zerbini
la source
-4

Y a-t-il une raison pour laquelle vous tapez un cast au intlieu d'analyser?

int speed1 = (int)(6.2f * 10)

lirait alors

int speed1 = Int.Parse((6.2f * 10).ToString()); 

La différence est probablement doubleliée à l'arrondi: si vous lancez, vous obtiendrez probablement quelque chose comme 61.78426.

Veuillez noter la sortie suivante

int speed1 = (int)(6.2f * 10);//61
double speed2 = (6.2f * 10);//61.9999980926514

C'est pourquoi vous obtenez des valeurs différentes!

Néo
la source
1
Int.Parseprend une chaîne comme paramètre.
ken2k
Vous ne pouvez analyser que des chaînes, je suppose que vous voulez dire pourquoi n'utilisez-vous pas System.Convert
vc 74