43 votes

La conversion d'un nombre décimal en nombre double en C# donne lieu à 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.

Le pire, c'est qu'il peut y avoir deux valeurs décimales "égales" qui donnent des valeurs doubles différentes lorsqu'elles sont converties.

Exemple 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 il ne peut pas s'agir d'un débordement (je suppose !).

Ce qui rend la chose encore plus intéressante, c'est qu'il peut y avoir deux égal des valeurs décimales (dans l'exemple de code ci-dessus, voir "dcm" et "dcm2", avec "deltaDcm" égal à zéro), ce qui se traduit par différents valeurs doubles lors de la conversion. (Dans le code, "dbl" et "dbl2", qui ont un "deltaDbl" non nul).

Je suppose que cela doit être lié à une différence dans la représentation binaire des nombres dans les deux types de données, mais je n'arrive pas à trouver quoi ! Et j'ai besoin de savoir ce qu'il faut faire pour que la conversion se fasse comme je le souhaite. (comme dcm2 -> dbl2)

49voto

Jon Skeet Points 692016

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

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

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 originale (8224055000.0000000000) qui est un entier - et exactement représentable comme un double - se retrouve avec des données supplémentaires. Je soupçonne fortement que cela est dû à des bizarreries dans l'algorithme utilisé pour convertir de decimal a double mais c'est malheureux.

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

Pour une conversion de décimal en flottant ou en double, la valeur décimale est arrondie à l'unité. la valeur double ou flottante la plus proche. Bien que cette conversion puisse entraîner une perte de précision, elle n'entraîne jamais la levée d'une exception. une exception.

La "valeur double la plus proche" est clairement 8224055000... il s'agit donc d'un bug. Je ne m'attends pas à ce qu'il soit corrigé de sitôt. (Il donne les mêmes résultats dans .NET 4.0b1 d'ailleurs).

Pour éviter ce bogue, vous voudrez probablement commencer par normaliser la valeur décimale, en "supprimant" les 0 supplémentaires après la virgule. Cette opération est quelque peu délicate, car elle implique l'arithmétique des nombres entiers sur 96 bits - la norme .NET 4.0 BigInteger La classe pourrait bien faciliter les choses, mais ce n'est peut-être pas une option pour vous.

25voto

Anton Tykhyy Points 12680

La réponse réside dans le fait que decimal tente de préserver le nombre de chiffres significatifs. Ainsi, 8224055000.0000000000m a 20 chiffres significatifs et est stocké sous la forme 82240550000000000000E-10 alors que 8224055000m n'a que 10 et est stocké comme 8224055000E+0 . double Sa mantisse est (logiquement) de 53 bits, soit au maximum 16 chiffres décimaux. C'est exactement la précision que vous obtenez lorsque vous convertissez en double et en effet, l'égaré 1 dans votre exemple est à la 16ème décimale. La conversion n'est pas de 1 à 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 du signe, de l'exposant et de la mantisse ; pour le décimal, voir MSDN sur decimal.GetBits mais essentiellement les 96 derniers bits sont la mantisse. Notez comment les bits de la mantisse de dcm2 et les bits les plus significatifs de dbl2 coïncident exactement (n'oubliez pas l'implicite 1 dans double ), et en fait ces bits représentent 8224055000. Les bits de la 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 de fin de ligne.

// 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") ;

5voto

Greg Hewgill Points 356191

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 l'on pourrait imaginer. Cela s'explique par le fait que les processeurs font de l'arithmétique en base 2, alors que les humains font (généralement) de l'arithmétique en base 10. Il existe une grande variété d'effets inattendus qui en découlent.

2voto

Ilan Points 125

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

Il me semble logiquement incorrect que la précision de la décimale puisse donner lieu à 3 doubles différents. Bravo à @AntonTykhyy pour l'idée de /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

1voto

pavium Points 7845

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

El simpliste L'explication 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.

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