web-dev-qa-db-fra.com

La conversion d'un nombre décimal en nombre double en C # entraîne une différence

Résumé du problème:

Pour certaines valeurs décimales, lorsque nous convertissons le type de décimal en double, une petite fraction est ajoutée au résultat.

Ce qui aggrave la situation, c'est qu'il peut y avoir deux valeurs décimales "égales" qui entraînent des valeurs doubles différentes lors de la conversion.

Échantillon de code:

decimal dcm = 8224055000.0000000000m;  // dcm = 8224055000
double dbl = Convert.ToDouble(dcm);    // dbl = 8224055000.000001

decimal dcm2 = Convert.ToDecimal(dbl); // dcm2 = 8224055000
double dbl2 = Convert.ToDouble(dcm2);  // dbl2 = 8224055000.0

decimal deltaDcm = dcm2 - dcm;         // deltaDcm = 0
double deltaDbl = dbl2 - dbl;          // deltaDbl = -0.00000095367431640625

Regardez les résultats dans les commentaires. Les résultats sont copiés à partir de la montre du débogueur. Les nombres qui produisent cet effet ont beaucoup moins de chiffres décimaux que la limite des types de données, donc ça ne peut pas être un débordement (je suppose!).

Ce qui le rend beaucoup plus intéressant, c'est qu'il peut y avoir deux égal valeurs décimales (dans l'exemple de code ci-dessus, voir "dcm" et "dcm2", avec "deltaDcm" égal à zéro) résultant en différent doubler les valeurs lors de la conversion. (Dans le code, "dbl" et "dbl2", qui ont un "deltaDbl" non nul)

Je suppose que cela devrait être lié à la différence dans la représentation au niveau du bit des nombres dans les deux types de données, mais je ne peux pas comprendre quoi! Et je dois savoir quoi faire pour que la conversion soit telle que j'en ai besoin. (comme dcm2 -> dbl2)

43
Iravanchi

Intéressant - bien que je ne fasse généralement pas confiance aux méthodes normales d'écriture de valeurs à virgule flottante lorsque vous êtes intéressé par les résultats exacts.

Voici une démonstration un peu plus simple, en utilisant DoubleConverter.cs que j'ai utilisé plusieurs fois auparavant.

using System;

class Test
{
    static void Main()
    {
        decimal dcm1 = 8224055000.0000000000m;
        decimal dcm2 = 8224055000m;
        double dbl1 = (double) dcm1;
        double dbl2 = (double) dcm2;

        Console.WriteLine(DoubleConverter.ToExactString(dbl1));
        Console.WriteLine(DoubleConverter.ToExactString(dbl2));
    }
}

Résultats:

8224055000.00000095367431640625
8224055000

Maintenant, la question est de savoir pourquoi la valeur d'origine (8224055000.0000000000) qui est un entier - et exactement représentable comme double - se retrouve avec des données supplémentaires. Je soupçonne fortement que cela est dû à des caprices dans l'algorithme utilisé pour convertir de decimal à double, mais c'est dommage.

Il viole également la section 6.2.1 de la spécification C #:

Pour une conversion de décimal en flottant ou double, la valeur décimale est arrondie à la valeur double ou flottante la plus proche. Bien que cette conversion puisse perdre en précision, elle ne provoque jamais la levée d'une exception.

La "valeur double la plus proche" est clairement 8224055000 ... c'est donc un bug IMO. Ce n'est pas celui que je m'attendrais à résoudre rapidement. (Cela donne d'ailleurs les mêmes résultats dans .NET 4.0b1.)

Pour éviter le bogue, vous souhaiterez probablement normaliser d'abord la valeur décimale, "supprimant" effectivement les 0 supplémentaires après le point décimal. Ceci est quelque peu délicat car il implique une arithmétique d'entier de 96 bits - la classe .NET 4.0 BigInteger pourrait bien vous faciliter la tâche, mais ce n'est peut-être pas une option pour vous.

49
Jon Skeet

La réponse réside dans le fait que decimal tente de conserver le nombre de chiffres significatifs. Donc, 8224055000.0000000000m a 20 chiffres significatifs et est stocké sous la forme 82240550000000000000E-10, tandis que 8224055000m n'en a que 10 et est stocké comme 8224055000E+0. La mantisse de double est (logiquement) de 53 bits, soit au plus 16 chiffres décimaux. C'est exactement la précision que vous obtenez lorsque vous convertissez en double, et en effet le parasite 1 dans votre exemple est à la 16ème décimale. La conversion n'est pas 1 en 1 car double utilise la base 2.

Voici les représentations binaires de vos nombres:

dcm:
00000000000010100000000000000000 00000000000000000000000000000100
01110101010100010010000001111110 11110010110000000110000000000000
dbl:
0.10000011111.1110101000110001000111101101100000000000000000000001
dcm2:
00000000000000000000000000000000 00000000000000000000000000000000
00000000000000000000000000000001 11101010001100010001111011011000
dbl2 (8224055000.0):
0.10000011111.1110101000110001000111101101100000000000000000000000

Pour le double, j'ai utilisé des points pour délimiter les champs de signe, d'exposant et de mantisse; pour décimal, voir MSDN sur decimal.GetBits , mais essentiellement les 96 derniers bits sont la mantisse. Notez comment les bits de mantisse de dcm2 et les bits les plus significatifs de dbl2 coïncident exactement (n'oubliez pas l'implicite 1 bit dans la mantisse de double), et en fait ces bits représentent 8224055000. Les bits de mantisse de dbl sont les mêmes que dans dcm2 et dbl2 mais pour les méchants 1 dans le bit le moins significatif. L'exposant de dcm est 10 et la mantisse est 82240550000000000000.

Mise à jour II: Il est en fait très facile de supprimer les zéros à la fin.

// There are 28 trailing zeros in this constant —
// no decimal can have more than 28 trailing zeros
const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000m ;

// decimal.ToString() faithfully prints trailing zeroes
Assert ((8224055000.000000000m).ToString () == "8224055000.000000000") ;

// Let System.Decimal.Divide() do all the work
Assert ((8224055000.000000000m / PreciseOne).ToString () == "8224055000") ;
Assert ((8224055000.000010000m / PreciseOne).ToString () == "8224055000.00001") ;
25
Anton Tykhyy

L'article Ce que tout informaticien devrait savoir sur l'arithmétique à virgule flottante serait un excellent point de départ.

La réponse courte est que l'arithmétique binaire à virgule flottante est nécessairement une approximation , et ce n'est pas toujours l'approximation que vous devinez. C'est parce que les CPU font de l'arithmétique en base 2, tandis que les humains (généralement) font de l'arithmétique en base 10. Il y a une grande variété d'effets inattendus qui en découlent.

5
Greg Hewgill

Pour voir ce problème plus clairement illustré, essayez ceci dans LinqPad (ou remplacez tous les .Dump () et passez à Console.WriteLine () si vous le souhaitez).

Il me semble logiquement incorrect que la précision de la décimale pourrait aboutir à 3 doubles différents. Félicitations à @AntonTykhyy pour l'idée/PreciseOne:

((double)200M).ToString("R").Dump(); // 200
((double)200.0M).ToString("R").Dump(); // 200
((double)200.00M).ToString("R").Dump(); // 200
((double)200.000M).ToString("R").Dump(); // 200
((double)200.0000M).ToString("R").Dump(); // 200
((double)200.00000M).ToString("R").Dump(); // 200
((double)200.000000M).ToString("R").Dump(); // 200
((double)200.0000000M).ToString("R").Dump(); // 200
((double)200.00000000M).ToString("R").Dump(); // 200
((double)200.000000000M).ToString("R").Dump(); // 200
((double)200.0000000000M).ToString("R").Dump(); // 200
((double)200.00000000000M).ToString("R").Dump(); // 200
((double)200.000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000M).ToString("R").Dump(); // 200
((double)200.000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.0000000000000000000000M).ToString("R").Dump(); // 200
((double)200.00000000000000000000000M).ToString("R").Dump(); // 200.00000000000003
((double)200.000000000000000000000000M).ToString("R").Dump(); // 200
((double)200.0000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997
((double)200.00000000000000000000000000M).ToString("R").Dump(); // 199.99999999999997

"\nFixed\n".Dump();

const decimal PreciseOne = 1.000000000000000000000000000000000000000000000000M;
((double)(200M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.0000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
((double)(200.00000000000000000000000000M/PreciseOne)).ToString("R").Dump(); // 200
2
Ilan

Il s'agit d'un ancien problème qui a fait l'objet de nombreuses questions similaires sur StackOverflow.

L'explication simpliste est que les nombres décimaux ne peuvent pas être représentés exactement en binaire

Ce lien est un article qui pourrait expliquer le problème.

1
pavium