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.
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.
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).
DoubleConverter.ToExactString((6.2f * 10))
// output 61.9999980926513671875
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 ...
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
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.
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