Benutzer mit den meisten Antworten
Genauigkeit und Rundungen

Frage
-
Hallo zusammen,
ich habe mal wieder ein Problem mit Genauigkeiten und Microsoft.
Ich habe ein Datenbankfeld (DR("Value")), Typ Objekt, was ich in eine Zahl wandeln will. Dazu habe ich 2 Funktionen, welche mir die Zahl entweder als Single(checkDBNull) oder Double(dbNulltoDbl) zurück liefert. Der obige Screenshot ist aus Visual Studio 2017.
Das Datenbankfeld enthalt den Wert 0.475. Wenn der Wert in eine Singlezahl (*100) gewandelt wird kommt 47.5 raus. In eine Doublezahl (*100) kommt 47.5 raus. Gerundet: Single = 47, Double = 48
und Single (47.5) ungleich Double(47.5)
Da Frage ich mich, ob die bei Microsoft zu viel rauchen!
Gibt es irgendwo eine Einstellung, die hilft?
Grüße
Stefan
Antworten
-
Hi Stefan,
vermutlich meinst Du das 5. Semester Mathe in der Uni:-) Das hilft, um zu verstehen, wie binäre Gleitkommazahlen aussehen, wie Prozessoren rechnen und was bei der Endlichkeit der Mantissen passieren kann.Zur Demo mal eine Methode einer Konsolenanwendung.
{ decimal var1 = 47.5m; var res1 = Math.Round(var1); Console.WriteLine($"Decimal {var1}, gerundet: {res1}"); decimal var2 = 46.5m; var res2 = Math.Round(var2); // mathematisch gerundet Console.WriteLine($"Decimal {var2}, gerundet mathematisch: {res2}"); var res3 = Math.Round(var2, 0, MidpointRounding.AwayFromZero); // kaufmännisch gerundet Console.WriteLine($"Decimal {var2}, gerundet kaufmännisch: {res3}"); Single var4 = 0.475f;
Single var5 = var4 * 100;
var res5 = Math.Round(var5); // mathematisch gerundet
Console.WriteLine($"Single {var4} * 100, gerundet mathematisch: {res5}");
double var6 = var4;
double var7 = var6 * 100;
var res7 = Math.Round(var7); // mathematisch gerundet
Console.WriteLine($"Double von Single-Zuweisung {var6}, gerundet mathematisch: {res7}");}
Das Ergebnis sieht so aus:
Decimal 47,5, gerundet: 48
Decimal 46,5, gerundet mathematisch: 46
Decimal 46,5, gerundet kaufmännisch: 47
Single 0,475 * 100, gerundet mathematisch: 48
Double von Single-Zuweisung 0,474999994039536, gerundet mathematisch: 47Eine Gleitkommazahl wird binär normalisiert abgespeichert (ich hoffe, Du verstehst, was Normalisierung ist, da das in der 5. Schulklasse noch nicht gelehrt wird). Und da kann auch ein 47,5 bei der Umwandlung von einer dezimalen Darstellung einer Zeichenkette in eine binäre Gleitkommazahl eine Periode entstehen, die wegen der endlichen Bitzahl der Mantisse teilweise abgeschnitten wird. Mit der Zuweisung einer Single zu einer Double zur Laufzeit wird wegen der widening conversion die Mantisse rechtsbündig mit Nullen aufgefüllt. Die Konvertierung dieser double in eine Zeichenkette (beim Console.WriteLine über die ToString-Methode) zeigt dann, dass es zu Rundungs-Abweichungen kommen kann.
Eine saubere Programmierung setzt voraus, dass man etwas weniger raucht und die Technik versteht :-)
Hauptregel ist, die richtigen Typen anzuwenden. Speziell für die Vermeidung der Rundungs-Abweichungen gibt es den Typ decimal bzw. money. Wenn doch double oder single genutzt werden müssen, dann ist das auch durchgängig zu machen und in Umwandlungsroutinen sind Abweichungen mittels Round zu eliminieren. Wichtig dabei ist, dass statistische Schieflagen infolge kaufmännischer Rundungen berücksichtigt werden.
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks
- Bearbeitet Peter Fleischer Freitag, 19. Januar 2018 20:28
- Als Antwort vorgeschlagen Guido Franzke Montag, 22. Januar 2018 07:25
- Als Antwort markiert Ivan DragovMicrosoft contingent staff, Moderator Mittwoch, 31. Januar 2018 06:29
Alle Antworten
-
Hi Stefan,
das Problem liegt nicht bei Microsoft, sondern bei dem vor dem Bildschirm, der vermutlich vom zu vielen Rauchen die Theorie nicht verstanden hat :-)Eine gebrochene Dezimalzahl kann u.U. nicht genau in eine binäre Gleitkommazahl umgewandelt werden. Z.B. ergibt 1/3 + 1/3 + 1/3 genau 1. Aber bei Mantissendarstellung ergibt 0,33 + 0,33 + 0,33 = 0,99. Da single und double unterschiedliche Mantissenlängen haben, wird unterschiedlich abgeschnitten und beim Vergleich gibt es keinen Treffer, z.B. 1/3 = 1/3, aber 0,33 <> 0,3333.
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Bearbeitet Peter Fleischer Freitag, 19. Januar 2018 19:38
-
Hallo Peter,
vom Prinzip her ist das korrekt.
Wenn aber die Rundung von 47.5 eine 47 ergibt, ist das trotzdem falsch. Es wird hier auch nicht geteilt oder sonstige Funktionen ausgeführt, sondern nur gerundet.
Du kannst es wenden wie du willst, 47.5 gerundet ergibt nicht 47.
Und eine periodische Zahl sehe ich dabei schon gar nicht. Da hilft auch nicht deine 5 Klasse Mathematik.
Davon ab, hast du meine Frage nach einer Lösung nicht beantwortet.
Grüße
-
Hallo,
das hat mir "normaler" Mathematik nichts zu tun. Die unterschiedlichen Datentypen sind unterschiedlich genau. Decimal ist sehr genau verbraucht aber viel mehr speicher als als z.B. Int. Float ist z.B. im Bereich zwischen 0 und 1 stabil, bei anderen bereichen könnten Probleme auftreten. Die Lösung ist also sich mit den Datentypen auseinander zu setzen und wissen warum man welchen Datentyp benutzt. Kosten zu nutzen sollten immer bedacht werden.
Das ganze Thema ist sehr komplex, man müsste damit anfangen was dem Entwickler für Datentypen zur Verfügung stehen wie der Compiler damit umgeht, wie diesen Datentypen im Arbeitsspeichern dargestellt werden und sie vom der CPU verarbeitet werden. Darüber könnte man ein Buch schreiben. Demnach solltest Du ein Buch in die Hand nehmen und dich einlesen. Erst dann kannst Du dir anmaßen über irgendwem zu spotten.
Gruß Thomas
Sage nie, ich kann es nicht - sage nur, ich kann es noch nicht!
Dev Apps von mir: Icon für UWP, UI Strings
Andere Dev Apps: UWP Community Toolkit Sample App- Bearbeitet Thomas Wycichowski Freitag, 19. Januar 2018 18:48
-
Hi Stefan,
vermutlich meinst Du das 5. Semester Mathe in der Uni:-) Das hilft, um zu verstehen, wie binäre Gleitkommazahlen aussehen, wie Prozessoren rechnen und was bei der Endlichkeit der Mantissen passieren kann.Zur Demo mal eine Methode einer Konsolenanwendung.
{ decimal var1 = 47.5m; var res1 = Math.Round(var1); Console.WriteLine($"Decimal {var1}, gerundet: {res1}"); decimal var2 = 46.5m; var res2 = Math.Round(var2); // mathematisch gerundet Console.WriteLine($"Decimal {var2}, gerundet mathematisch: {res2}"); var res3 = Math.Round(var2, 0, MidpointRounding.AwayFromZero); // kaufmännisch gerundet Console.WriteLine($"Decimal {var2}, gerundet kaufmännisch: {res3}"); Single var4 = 0.475f;
Single var5 = var4 * 100;
var res5 = Math.Round(var5); // mathematisch gerundet
Console.WriteLine($"Single {var4} * 100, gerundet mathematisch: {res5}");
double var6 = var4;
double var7 = var6 * 100;
var res7 = Math.Round(var7); // mathematisch gerundet
Console.WriteLine($"Double von Single-Zuweisung {var6}, gerundet mathematisch: {res7}");}
Das Ergebnis sieht so aus:
Decimal 47,5, gerundet: 48
Decimal 46,5, gerundet mathematisch: 46
Decimal 46,5, gerundet kaufmännisch: 47
Single 0,475 * 100, gerundet mathematisch: 48
Double von Single-Zuweisung 0,474999994039536, gerundet mathematisch: 47Eine Gleitkommazahl wird binär normalisiert abgespeichert (ich hoffe, Du verstehst, was Normalisierung ist, da das in der 5. Schulklasse noch nicht gelehrt wird). Und da kann auch ein 47,5 bei der Umwandlung von einer dezimalen Darstellung einer Zeichenkette in eine binäre Gleitkommazahl eine Periode entstehen, die wegen der endlichen Bitzahl der Mantisse teilweise abgeschnitten wird. Mit der Zuweisung einer Single zu einer Double zur Laufzeit wird wegen der widening conversion die Mantisse rechtsbündig mit Nullen aufgefüllt. Die Konvertierung dieser double in eine Zeichenkette (beim Console.WriteLine über die ToString-Methode) zeigt dann, dass es zu Rundungs-Abweichungen kommen kann.
Eine saubere Programmierung setzt voraus, dass man etwas weniger raucht und die Technik versteht :-)
Hauptregel ist, die richtigen Typen anzuwenden. Speziell für die Vermeidung der Rundungs-Abweichungen gibt es den Typ decimal bzw. money. Wenn doch double oder single genutzt werden müssen, dann ist das auch durchgängig zu machen und in Umwandlungsroutinen sind Abweichungen mittels Round zu eliminieren. Wichtig dabei ist, dass statistische Schieflagen infolge kaufmännischer Rundungen berücksichtigt werden.
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks
- Bearbeitet Peter Fleischer Freitag, 19. Januar 2018 20:28
- Als Antwort vorgeschlagen Guido Franzke Montag, 22. Januar 2018 07:25
- Als Antwort markiert Ivan DragovMicrosoft contingent staff, Moderator Mittwoch, 31. Januar 2018 06:29
-
Hallo,
selbst als Nichtraucher finde ich bemerkenswert, zumindest gewöhnungsbedürftig ;-)
float f100 = 100f; Console.WriteLine(Math.Round(0.475f * f100, 0, MidpointRounding.AwayFromZero)); // 47 Console.WriteLine(Math.Round(0.475f * 100f, 0, MidpointRounding.AwayFromZero)); // 48
Gruß
-
Hi,
ei mir bringen beide Anweisungen als Ergebnis 48.Unterschiede gibt es im folgenden Fall:
double d100 = 100; Console.WriteLine(Math.Round(0.475f * d100, 0, MidpointRounding.AwayFromZero)); // 47 Console.WriteLine(Math.Round(0.475f * 100f, 0, MidpointRounding.AwayFromZero)); // 48
Ursache ist die genutzte Überladung des *-Operators, der für den ersten Fall eine widening conversion des float-Literals 0.475f zu double einschließt.
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Bearbeitet Peter Fleischer Dienstag, 23. Januar 2018 05:35
-
Hallo Peter,
dann ist es aber doch zumindest erstaunlich, wenn dein Rechner zu anderen Ergebnissen kommt als meiner. Im Übrigen hast du aber meinen Code verändert und kommst deshalb zu anderen Ergebnissen.
Daneben frage ich mich, auf welcher Basis hier verschiedene Operator-Überladungen ausgewählt werden könnten. In beiden Zeilen wird Type-sicher float * float aufgerufen.
Wie immer das begründet werden könnte, finde ich, ist das ein überraschendes Ergebnis. Meines Erachtens sollte das nicht sein.
Gruß
-
Hi,
Dein Code bringt bei mir in beiden Fällen als Ergebnis 48. Ich nutze VS 2017, Framework 4.7. Wenn man mit dem Cursor auf den *-Operator geht, sieht man, dass bei Dir in beiden Fällen die Float-Überladung genutzt wird.In meinem Beispiel nutze ich im ersten Fall eine double-Variable und der Compiler nutzt deshalb im Fall, wenn einer der Operanden vom Typ double ist, die double-Überladung, was eine widening conversion des float-Literals bedingt.
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks
- Bearbeitet Peter Fleischer Dienstag, 23. Januar 2018 10:47 Grammatik
-
Gut, dann kommen wir zu unterschiedlichen Resultaten (was ja eigentlich auch nicht sein sollte). Ich habe das noch mal auf 2 weiteren Rechnern getestet mit jeweils mein ursprüngliches Resultat bestätigenden Ergebnissen.
Auf einem anderen Rechner habe ich noch VS2010 installiert. Da liefert mir das für beide Varianten jeweils 47.
Gruß
-
Hi,
das ist interessant und ich würde es gern weiter verfolgen.Bei mir (VS 2017, FW 4.7, Any, 64 Bit CPU) kommt folgendes:
float f100 = 100f; Console.WriteLine(Math.Round(0.475f * f100, 0, MidpointRounding.AwayFromZero)); // 48 var res11 = 0.475f * f100; Console.WriteLine(res11.ToString() + " - " + res11.GetType()); Console.WriteLine(Math.Round(0.475f * 100f, 0, MidpointRounding.AwayFromZero)); // 48 var res12 = 0.475f * 100f; Console.WriteLine(res12.ToString() + " - " + res12.GetType()); Console.WriteLine(); double d100 = 100; Console.WriteLine(Math.Round(0.475f * d100, 0, MidpointRounding.AwayFromZero)); // 47 var res13 = 0.475f * d100; Console.WriteLine(res13.ToString() + " - " + res13.GetType()); Console.WriteLine(Math.Round(0.475f * 100f, 0, MidpointRounding.AwayFromZero)); // 48 var res14 = 0.475f * 100f; Console.WriteLine(res14.ToString() + " - " + res14.GetType()); 48 47,5 - System.Single 48 47,5 - System.Single 47 47,4999994039536 - System.Double 48 47,5 - System.Single
Kannst Du den Codeschnipsel bei Dir mal abarbeiten, das Ergebnis posten und kurz die Umgebung beschreiben (VS-Versio, FW-Version, X86/x64/ARM)
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Bearbeitet Peter Fleischer Dienstag, 23. Januar 2018 10:57
-
Ja, sicher doch ;-)
Hier die Resultate:
47 47,5 - System.Single 48 47,5 - System.Single 47 47,4999994039536 - System.Double 48 47,5 - System.Single
Verfahrensweise:
Ich habe verschiedene Einstellungen durchprobiert (von c#3 - c#7.2) und gegen verschiedene Frameworks kompiliert (von 4.0 - 4.7.1). Vor jedem Durchlauf wurde "Projekt bereinigen" ausgeführt.
Ich will es zwar nicht ausschließen (habe hier nur verschiedene Intel-Prozessoren, i7, Xeon, ...), glaube aber nicht, dass es daran liegt.
Ansonsten habe ich nichts Spezielles im Köcher.
Wenn du nun einmal VS beendest, neu startest, ein neues Konsolenprojekt erstellst, nur meine 3 Codezeilen einfügst, bekommst du dann immer noch 48/48 raus?
Gruß
- Bearbeitet K. Pater Dienstag, 23. Januar 2018 11:16
-
So,
nun gibt es doch einen Unterschied:
Kompiliert für x86:
47 47,5 - System.Single 48 47,5 - System.Single 47 47,4999994039536 - System.Double 48 47,5 - System.Single
und kompiliert für x64/Any:
48 47,5 - System.Single 48 47,5 - System.Single 47 47,4999994039536 - System.Double 48 47,5 - System.Single
Das dürfte es klar machen...
Kannst du das so bestätigen?
Gruß
- Bearbeitet K. Pater Dienstag, 23. Januar 2018 11:25
-
Hi,
ich vermute, dass es mit der unterschiedlichen Implementierung der Extended precision zu tun hat, die in 32-Bit und 64-Bit Architekturen unterschiedlich implementiert ist. In einer 32-Bit Architektur wird das Word (4 Byte) auf Basis des letzten Bites für die Verarbeitung im 8087-Teil erweitert, was dann zu Unterschieden gegenüber der Verarbeitung der 8 Byte im X64 führt.
Hier noch etwas: Differences between 32 and 64-bit .NET (4) applications
x64 managed code will use Streaming SIMD Extensions (SSE) for double/float computation instead of x87 Floating Point Unit (FPU) when using x86 managed code.
--
Viele Grüsse
Peter Fleischer (ehem. MVP)
Meine Homepage mit Tipps und Tricks- Bearbeitet Peter Fleischer Dienstag, 23. Januar 2018 11:51