it-swarm.com.de

Doppelte Formatierung für Ausgabe in C #

Ausführen eines schnellen Experiments zu Ist doppelte Multiplikation in .NET defekt? und ein paar Artikel über die Formatierung von C # -Zeichen gelesen, dachte ich:

{
    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));
}

Wäre das C # -Äquivalent dieses C-Codes:

{
    double i = 10 * 0.69;

    printf ( "%f\n", i );
    printf ( "  %.20f\n", i );
    printf ( "+ %.20f\n", 6.9 - i );
    printf ( "= %.20f\n", 6.9 );
}

Das C # erzeugt jedoch die Ausgabe:

6.9
  6.90000000000000000000
+ 0.00000000000000088818
= 6.90000000000000000000

obwohl ich im Debugger gleich 6.8999999999999999946709 (statt 6.9) auftauche.

im Vergleich zu C, das die vom Format geforderte Genauigkeit zeigt:

6.900000                          
  6.89999999999999946709          
+ 0.00000000000000088818          
= 6.90000000000000035527          

Was ist los?

(Microsoft .NET Framework Version 3.51 SP1/Visual Studio C # 2008 Express Edition)


Ich habe einen Hintergrund in der numerischen Berechnung und Erfahrung mit der Implementierung von Intervallarithmetik - einer Technik zum Schätzen von Fehlern aufgrund der Genauigkeitsgrenzen in komplizierten numerischen Systemen - auf verschiedenen Plattformen. Um die Belohnung zu erhalten, versuchen Sie nicht, die Speichergenauigkeit zu erklären. In diesem Fall ist dies ein Unterschied von einem ULP eines 64-Bit-Doppels. 

Um die Bounty zu erhalten, möchte ich wissen, wie (oder ob) .Net ein doppeltes Format so formatieren kann, wie es im C-Code sichtbar ist. 

58
Pete Kirkham

Das Problem ist, dass .NET immer eine double auf 15 signifikante Dezimalstellen vor rundet, unabhängig von der von Ihrem Format geforderten Genauigkeit und unabhängig vom genauen Dezimalwert der Binärzahl.

Ich vermute, dass der Visual Studio-Debugger über eigene Format-/Anzeigeroutinen verfügt, die direkt auf die interne Binärzahl zugreifen, daher die Diskrepanzen zwischen C # -Code, C-Code und Debugger.

Es gibt nichts eingebautes, das es Ihnen ermöglicht, auf den genauen Dezimalwert einer double zuzugreifen oder eine double auf eine bestimmte Anzahl von Dezimalstellen zu formatieren. Sie können dies jedoch selbst tun, indem Sie die interne Binärzahl auseinander nehmen und neu aufbauen es ist eine Zeichenfolgendarstellung des Dezimalwerts.

Alternativ können Sie Jon Skeets DoubleConverter-Klasse verwenden (verknüpft mit seinem "Binary Floating Point und .NET-Artikel ). Diese hat eine ToExactString-Methode, die den exakten Dezimalwert einer double zurückgibt. Sie können dies leicht ändern, um die Rundung der Ausgabe auf eine bestimmte Genauigkeit zu ermöglichen.

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
57
LukeH
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);
21
user240141

Schauen Sie sich diese MSDN-Referenz an. In den Anmerkungen heißt es, dass die Zahlen auf die Anzahl der angeforderten Dezimalstellen gerundet werden.

Wenn Sie stattdessen "{0: R}" verwenden, wird ein so genannter "Round-Trip" -Wert erzeugt. Schauen Sie sich diese MSDN-Referenz an für weitere Informationen, hier ist mein Code und die Ausgabe:

double d = 10 * 0.69;
Console.WriteLine("  {0:R}", d);
Console.WriteLine("+ {0:F20}", 6.9 - d);
Console.WriteLine("= {0:F20}", 6.9);

Ausgabe

  6.8999999999999995
+ 0.00000000000000088818
= 6.90000000000000000000
8
Timothy Walters

Obwohl diese Frage inzwischen geschlossen ist, ist es meiner Meinung nach erwähnenswert, wie diese Grausamkeit entstanden ist. In gewisser Weise können Sie die C # -Spezifikation beschuldigen, die besagt, dass ein Double eine Genauigkeit von 15 oder 16 Stellen haben muss (das Ergebnis von IEEE-754). Etwas weiter (Abschnitt 4.1.6) heißt es, dass Implementierungen die höhere - Genauigkeit verwenden dürfen. Wohlgemerkt: höher, nicht niedriger. Sie dürfen sogar von IEEE-754 abweichen: Ausdrücke vom Typ x * y / z, wobei x * y+/-INF ergeben würde, sich aber nach der Division in einem gültigen Bereich befinden würde, müssen nicht zu einem Fehler führen. Diese Funktion erleichtert Compilern die Verwendung einer höheren Genauigkeit in Architekturen, bei denen dies zu einer besseren Leistung führt.

Aber ich habe einen "Grund" versprochen. Hier ist ein Zitat (Sie haben in einem Ihrer letzten Kommentare eine Ressource angefordert) von der CLI für Shared Source , in clr/src/vm/comnumber.cpp:

"Um Zahlen zu geben, die sowohl __. Freundlich als auch Round-trippable sind, analysieren wir die Nummer .__ unter Verwendung von 15 Ziffern und bestimmen dann, ob Rundfahrten auf denselben Wert ausgeführt werden. Andernfalls konvertieren wir diese NUMMER in eine Zeichenfolge. Andernfalls verwenden wir 17 Ziffern und zeigen diese an. "

Mit anderen Worten: Das CLI-Entwicklungsteam von MS hat beschlossen, sowohl Roundtrip-fähig zu sein als auch schöne Werte zu zeigen, die nicht so schwer zu lesen sind. Gut oder schlecht? Ich würde mir eine Opt-In oder Opt-Out wünschen.

Der Trick, um herauszufinden, ob eine beliebige Anzahl von Hin- und Rückläufen möglich ist? Konvertierung in eine generische NUMBER-Struktur (mit separaten Feldern für die Eigenschaften eines Double) und zurück, und vergleichen Sie dann, ob das Ergebnis unterschiedlich ist. Wenn es anders ist, wird der genaue Wert verwendet (wie in Ihrem mittleren Wert mit 6.9 - i). Wenn er gleich ist, wird der "hübsche Wert" verwendet. 

Wie Sie bereits in einem Kommentar zu Andyp bemerkt haben, ist 6.90...00 bitweise gleich 6.89...9467. Und jetzt wissen Sie, warum 0.0...8818 verwendet wird: Es unterscheidet sich bitweise von 0.0.

Diese 15-stellige Barriere ist hartcodiert und kann nur durch Neukompilieren der CLI, mithilfe von Mono oder durch Aufrufen von Microsoft und der Überzeugung geändert werden, eine Option zum Drucken der vollen "Genauigkeit" hinzuzufügen (es ist nicht wirklich Präzision, aber durch das Fehlen eines besseren Wortes). Es ist wahrscheinlich einfacher, die 52-Bit-Genauigkeit selbst zu berechnen oder die zuvor erwähnte Bibliothek zu verwenden.

BEARBEITEN: Wenn Sie sich gerne mit IEE-754-Gleitkommazahlen experimentieren möchten, betrachten Sie dieses Online-Tool , das Ihnen alle relevanten Teile eines Gleitkommawerts zeigt.

8
Abel

Benutzen

Console.WriteLine(String.Format("  {0:G17}", i));

Das gibt Ihnen alle 17 Ziffern, die es hat. Standardmäßig enthält ein Double-Wert eine Genauigkeit von 15 Dezimalstellen, obwohl intern maximal 17 Stellen gespeichert werden. {0: R} liefert nicht immer 17 Ziffern, sondern 15, wenn die Zahl mit dieser Genauigkeit dargestellt werden kann.

das gibt 15 Ziffern zurück, wenn die Zahl mit dieser Genauigkeit dargestellt werden kann, oder 17 Ziffern, wenn die Anzahl nur mit maximaler Genauigkeit dargestellt werden kann. Es gibt nichts, was Sie tun können, um die doppelte Rückgabe so zu gestalten, wie sie implementiert wird. Wenn es Ihnen nicht gefällt, machen Sie eine neue Doppelklasse ...

Die doppelte Überhöhung von .NET kann nicht mehr als 17 Stellen speichern. Sie können also 6.89999999999999946709 im Debugger nicht als 6.8999999999999995 anzeigen. Bitte machen Sie ein Bild, um uns zu beweisen.

4
ceciliaSHARP

Die Antwort darauf ist einfach und kann unter MSDN gefunden werden.

Denken Sie daran, dass eine Gleitkommazahl nur eine Dezimalzahl approximieren kann und dass die Genauigkeit einer Gleitkommazahl bestimmt, wie genau diese Zahl einer Dezimalzahl entspricht. Standardmäßig enthält ein Double-Wert 15 Dezimalstellen der Genauigkeit, obwohl intern maximal 17 Stellen gespeichert werden.

In Ihrem Beispiel ist der Wert von i 6,89999999999999946709, der die Nummer 9 für alle Positionen zwischen der 3. und 16. Stelle hat (denken Sie daran, den ganzzahligen Teil in den Ziffern zu zählen). Bei der Umwandlung in einen String rundet das Framework die Zahl auf die 15. Stelle.

i     = 6.89999999999999 946709
digit =           111111 111122
        1 23456789012345 678901
2
Marcel Gosselin

ich habe versucht, Ihre Ergebnisse zu reproduzieren, aber als ich "i" im Debugger sah, wurde "6,8999999999999995" angezeigt und nicht "6.89999999999999946709", wie Sie in der Frage geschrieben haben. Können Sie Schritte geben, um das Gesehene zu reproduzieren?

Um zu sehen, was der Debugger Ihnen anzeigt, können Sie einen DoubleConverter wie in der folgenden Codezeile verwenden:

Console.WriteLine(TypeDescriptor.GetConverter(i).ConvertTo(i, typeof(string)));

Hoffe das hilft!

Edit: Ich denke, ich bin müder, als ich dachte, natürlich ist dies das Gleiche wie das Formatieren auf den Roundtrip-Wert (wie zuvor erwähnt).

2
andyp

Die Antwort ist ja, der doppelte Druck ist in .NET fehlerhaft, es werden nachstehende Müllzahlen gedruckt.

Sie können lesen, wie Sie es richtig implementieren hier .

Ich musste das gleiche für IronScheme tun.

> (* 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

Hinweis: Sowohl C als auch C # haben einen korrekten Wert, der Druck ist nur unterbrochen.

Update: Ich suche immer noch nach der Mailinglisten-Konversation, die ich zu dieser Entdeckung geführt habe.

0
leppie

Ich habe diese schnelle Lösung gefunden.

    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 );
0
Suneesh