Exécution d’une expérience rapide liée à La double multiplication est-elle rompue dans .NET? et en lisant quelques articles sur le formatage de chaîne C #, je me suis dit que:
{
double i = 10 * 0.69;
Console.WriteLine(i);
Console.WriteLine(String.Format(" {0:F20}", i));
Console.WriteLine(String.Format("+ {0:F20}", 6.9 - i));
Console.WriteLine(String.Format("= {0:F20}", 6.9));
}
Serait l'équivalent C # de ce code C:
{
double i = 10 * 0.69;
printf ( "%f\n", i );
printf ( " %.20f\n", i );
printf ( "+ %.20f\n", 6.9 - i );
printf ( "= %.20f\n", 6.9 );
}
Cependant le C # produit la sortie:
6.9
6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000
malgré que je sois égal à la valeur 6.8999999999999999946709 (plutôt que 6.9) dans le débogueur.
comparé à C qui montre la précision demandée par le format:
6.900000
6.89999999999999946709
+ 0.00000000000000088818
= 6.90000000000000035527
Que se passe-t-il?
(Microsoft .NET Framework version 3.51 SP1/Visual Studio C # 2008 Express Edition)
J'ai une formation en calcul numérique et une expérience dans la mise en œuvre de l'arithmétique par intervalles, une technique permettant d'estimer les erreurs dues aux limites de précision dans les systèmes numériques complexes, sur diverses plates-formes. Pour obtenir la prime, n'essayez pas d'expliquer ce qu'est la précision de stockage. Dans ce cas, il s'agit d'une différence entre un ULP et un double 64 bits.
Pour obtenir la prime, je veux savoir comment. Net peut formater un double à la précision demandée, comme visible dans le code C.
Le problème est que .NET arrondira toujours une double
à 15 chiffres décimaux significatifs avant que n'applique votre mise en forme, quelle que soit la précision demandée par votre format et quelle que soit la valeur décimale exacte du nombre binaire.
J'imagine que le débogueur Visual Studio a ses propres routines de format/affichage qui accèdent directement au nombre binaire interne, d'où les différences entre votre code C #, votre code C et le débogueur.
Il n'y a rien d'intégré qui vous permette d'accéder à la valeur décimale exacte d'une double
ou de vous permettre de formater une double
en un nombre spécifique de décimales, mais vous pouvez le faire vous-même en séparant le nombre binaire interne et en le reconstruisant. il s'agit d'une représentation sous forme de chaîne de la valeur décimale.
Vous pouvez également utiliser la classe DoubleConverter
de Jon Skeet (liée à son article - "Binary float and .NET" ). Cela a une méthode ToExactString
qui retourne la valeur décimale exacte d'un double
. Vous pouvez facilement modifier cela pour permettre d'arrondir la sortie avec une précision spécifique.
double i = 10 * 0.69;
Console.WriteLine(DoubleConverter.ToExactString(i));
Console.WriteLine(DoubleConverter.ToExactString(6.9 - i));
Console.WriteLine(DoubleConverter.ToExactString(6.9));
// 6.89999999999999946709294817992486059665679931640625
// 0.00000000000000088817841970012523233890533447265625
// 6.9000000000000003552713678800500929355621337890625
Digits after decimal point
// just two decimal places
String.Format("{0:0.00}", 123.4567); // "123.46"
String.Format("{0:0.00}", 123.4); // "123.40"
String.Format("{0:0.00}", 123.0); // "123.00"
// max. two decimal places
String.Format("{0:0.##}", 123.4567); // "123.46"
String.Format("{0:0.##}", 123.4); // "123.4"
String.Format("{0:0.##}", 123.0); // "123"
// at least two digits before decimal point
String.Format("{0:00.0}", 123.4567); // "123.5"
String.Format("{0:00.0}", 23.4567); // "23.5"
String.Format("{0:00.0}", 3.4567); // "03.5"
String.Format("{0:00.0}", -3.4567); // "-03.5"
Thousands separator
String.Format("{0:0,0.0}", 12345.67); // "12,345.7"
String.Format("{0:0,0}", 12345.67); // "12,346"
Zero
Following code shows how can be formatted a zero (of double type).
String.Format("{0:0.0}", 0.0); // "0.0"
String.Format("{0:0.#}", 0.0); // "0"
String.Format("{0:#.0}", 0.0); // ".0"
String.Format("{0:#.#}", 0.0); // ""
Align numbers with spaces
String.Format("{0,10:0.0}", 123.4567); // " 123.5"
String.Format("{0,-10:0.0}", 123.4567); // "123.5 "
String.Format("{0,10:0.0}", -123.4567); // " -123.5"
String.Format("{0,-10:0.0}", -123.4567); // "-123.5 "
Custom formatting for negative numbers and zero
String.Format("{0:0.00;minus 0.00;zero}", 123.4567); // "123.46"
String.Format("{0:0.00;minus 0.00;zero}", -123.4567); // "minus 123.46"
String.Format("{0:0.00;minus 0.00;zero}", 0.0); // "zero"
Some funny examples
String.Format("{0:my number is 0.0}", 12.3); // "my number is 12.3"
String.Format("{0:0aaa.bbb0}", 12.3);
Jetez un oeil à cette référence MSDN . Dans les notes, il est indiqué que les nombres sont arrondis au nombre de décimales demandé.
Si vous utilisez plutôt "{0: R}", il produira ce que l'on appelle une valeur "aller-retour", jetez un coup d'œil à cette référence MSDN pour plus d'informations, voici mon code et le résultat:
double d = 10 * 0.69;
Console.WriteLine(" {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);
sortie
6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000
Bien que cette question soit réglée entre-temps, je pense qu’il est utile de mentionner comment cette atrocité a été créée. D'une certaine manière, vous pouvez blâmer la spécification C #, qui stipule qu'un double doit avoir une précision de 15 ou 16 chiffres (résultat de IEEE-754). Un peu plus loin (section 4.1.6), il est indiqué que les implémentations sont autorisées à utiliser la précision higher. Remarque: supérieur, pas inférieur. Ils sont même autorisés à s'écarter de IEEE-754: les expressions du type x * y / z
où x * y
produirait +/-INF
mais se trouverait dans une plage valide après la division, ne doivent pas nécessairement entraîner une erreur. Cette fonctionnalité permet aux compilateurs d’utiliser plus facilement la précision dans les architectures où cela donnerait de meilleures performances.
Mais j'ai promis une "raison". Voici une citation (vous avez demandé une ressource dans l'un de vos commentaires récents) à partir de CLI de source partagée , en clr/src/vm/comnumber.cpp
:
"Afin de donner aux nombres qui sont à la fois faciles à afficher et à arrondir, nous analysons le nombre à l'aide de 15 chiffres et déterminons ensuite si Arrondit à la même valeur. Si c'est le cas, nous convertissons ce NUMBER en chaîne Sinon, nous réparons à l'aide de 17 .__ chiffres et les affichons. "
En d’autres termes: l’équipe de développement de la CLI de MS a décidé d’être à la fois triplable et de montrer de jolies valeurs qui ne sont pas si pénibles à lire. Bon ou Mauvais? Je souhaiterais un opt-in ou opt-out.
Le truc est-il de trouver cette ronde-trippability d'un nombre donné? Conversion en une structure NUMBER générique (comportant des champs distincts pour les propriétés d'un double) et inversement, puis compare si le résultat est différent. Si elle est différente, la valeur exacte est utilisée (comme dans votre valeur intermédiaire avec 6.9 - i
) si elle est identique, la "jolie valeur" est utilisée.
Comme vous l'avez déjà remarqué dans un commentaire à Andyp, 6.90...00
est au niveau binaire égal à 6.89...9467
. Et maintenant vous savez pourquoi 0.0...8818
est utilisé: il est différent de 0.0
au niveau du bit.
Cette barrière de 15 chiffres est codée en dur et ne peut être modifiée qu’en recompilant la CLI, en utilisant Mono ou en appelant Microsoft pour les convaincre d’ajouter une option permettant d’imprimer une "précision" complète (ce n’est pas vraiment une précision, mais une précision suffisante). par le manque d'une meilleure parole). Il est probablement plus facile de calculer vous-même la précision de 52 bits ou d'utiliser la bibliothèque mentionnée précédemment.
EDIT: si vous aimez expérimenter vous-même les points flottants IEE-754, considérez cet outil en ligne , qui vous montre toutes les parties pertinentes d’un point flottant.
Utilisation
Console.WriteLine(String.Format(" {0:G17}", i));
Cela vous donnera tous les 17 chiffres qu'il a. Par défaut, une valeur Double contient une précision de 15 décimales, mais un maximum de 17 chiffres est conservé en interne. {0: R} ne vous donnera pas toujours 17 chiffres, il en donnera 15 si le nombre peut être représenté avec cette précision.
qui retourne 15 chiffres si le nombre peut être représenté avec cette précision ou 17 chiffres si le nombre ne peut être représenté qu'avec une précision maximale. Il n’ya rien que vous puissiez faire pour faire en sorte que le double retour ait plus de chiffres, c’est ainsi que cela est mis en œuvre. Si vous n'aimez pas cela, faites une nouvelle classe double vous-même ...
Le double de .NET ne peut stocker plus de chiffres que 17, vous ne pouvez donc pas voir 6.8999999999999999946709 dans le débogueur que vous verriez 6.8999999999999995. S'il vous plaît fournir une image pour nous prouver le contraire.
La réponse à cette question est simple et peut être trouvée sur MSDN
Rappelez-vous qu'un nombre à virgule flottante ne peut s'approcher que d'un nombre décimal, et que la précision d'un nombre à virgule flottante détermine la précision avec laquelle ce nombre se rapproche d'un nombre décimal. Par défaut, une valeur Double contient 15 chiffres décimaux de précision, bien qu’un maximum de 17 chiffres soit conservé en interne.
Dans votre exemple, la valeur de i est 6.8999999999999999946709, qui porte le numéro 9 pour toutes les positions comprises entre le 3ème et le 16ème chiffre (n'oubliez pas de compter la partie entière dans les chiffres). Lors de la conversion en chaîne, le cadre arrondit le nombre au 15ème chiffre.
i = 6.89999999999999 946709
digit = 111111 111122
1 23456789012345 678901
j'ai essayé de reproduire vos conclusions, mais lorsque j'ai regardé "i" dans le débogueur, il s'est avéré que "6.899999999999999995" n'était pas "6.8999999999999999946709" comme vous l'avez écrit dans la question. Pouvez-vous fournir des étapes pour reproduire ce que vous avez vu?
Pour voir ce que le débogueur vous montre, vous pouvez utiliser un DoubleConverter comme dans la ligne de code suivante:
Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));
J'espère que cela t'aides!
Edit: Je suppose que je suis plus fatigué que je ne le pensais, bien sûr, cela revient à formater à la valeur aller-retour (comme mentionné précédemment).
La réponse est oui, la double impression est interrompue dans .NET, ils sont en train d'imprimer les derniers chiffres de mémoire.
Vous pouvez lire comment l’appliquer correctement ici .
J'ai eu à faire de même pour IronScheme.
> (* 10.0 0.69)
6.8999999999999995
> 6.89999999999999946709
6.8999999999999995
> (- 6.9 (* 10.0 0.69))
8.881784197001252e-16
> 6.9
6.9
> (- 6.9 8.881784197001252e-16)
6.8999999999999995
Remarque: C et C # ont une valeur correcte, mais l’impression est cassée.
Mise à jour: je suis toujours à la recherche de la conversation que j'ai eue sur la liste de diffusion menant à cette découverte.
J'ai trouvé cette solution rapide.
double i = 10 * 0.69;
System.Diagnostics.Debug.WriteLine(i);
String s = String.Format("{0:F20}", i).Substring(0,20);
System.Diagnostics.Debug.WriteLine(s + " " +s.Length );