2009-03-30 8 views
7

Betrachten Sie das folgende C# -Code hinzu:Warum wirkt sich die um die Rundung, wenn mehrere Doppel in C#

double result1 = 1.0 + 1.1 + 1.2; 
double result2 = 1.2 + 1.0 + 1.1; 

if (result1 == result2) 
{ 
    ... 
} 

result1 sollte immer gleich result2 richtig? Die Sache ist, es tut es nicht. result1 ist 3.3 und result2 ist 3.3000000000000003. Der einzige Unterschied ist die Reihenfolge der Konstanten.

Ich weiß, dass Doppelgänger so implementiert sind, dass Rundungsprobleme auftreten können. Ich bin mir bewusst, dass ich stattdessen Dezimalzahlen verwenden kann, wenn ich absolute Präzision brauche. Oder dass ich Math.Round() in meiner if-Anweisung verwenden kann. Ich bin nur ein Nerd, der verstehen will, was der C# -Compiler tut. Kann mir das jemand sagen?

Edit:

Dank an alle, die bisher vorgeschlagenen auf Gleitkomma-Arithmetik zu lesen und/oder sprachen über die inhärente Ungenauigkeit, wie die CPU verdoppelt abwickelt. Aber ich glaube, der Hauptstoß meiner Frage ist noch immer unbeantwortet. Was ist meine Schuld, weil ich es nicht richtig formuliert habe? Lassen Sie es mich so sagen:

Brechen Sie den obigen Code nach unten, würde ich die folgenden Operationen erwarten zu geschehen: Die

double r1 = 1.1 + 1.2; 
double r2 = 1.0 + r1 
double r3 = 1.0 + 1.1 
double r4 = 1.2 + r3 

Nehmen wir an, dass jeder der oben genannten Ergänzungen einen Rundungsfehler (nummeriert e1 hatte. .e4). So enthält r1 den Rundungsfehler e1, r2 enthält Rundungsfehler e1 + e2, r3 enthält e3 und r4 enthält e3 + e4.

Nun weiß ich nicht, wie genau die Rundungsfehler passieren, aber ich hätte erwartet, dass e1 + e2 gleich e3 + e4 ist. Natürlich nicht, aber das scheint mir irgendwie falsch zu sein. Eine andere Sache ist, dass wenn ich den obigen Code ausführen, bekomme ich keine Rundungsfehler. Das ist es, was mich denken lässt, dass es der C# -Compiler ist, der eher etwas Seltsames macht als die CPU.

Ich weiß, ich frage viel und vielleicht die beste Antwort, die jemand geben kann, ist zu gehen und einen PHD in CPU-Design zu tun, aber ich dachte nur, ich würde fragen.

Edit 2

am IL Blick von meinem ursprünglichen Code-Beispiel, dann ist es klar, dass es der Compiler ist nicht die CPU, die dies tut:

.method private hidebysig static void Main(string[] args) cil managed 
{ 
.entrypoint 
    .maxstack 1 
    .locals init (
     [0] float64 result1, 
     [1] float64 result2) 
    L_0000: nop 
    L_0001: ldc.r8 3.3 
    L_000a: stloc.0 
    L_000b: ldc.r8 3.3000000000000003 
    L_0014: stloc.1 
    L_0015: ret 
} 

Der Compiler fügt die Zahlen nach oben für mich!

Antwort

10

ich erwartet hätte e1 + e2 gleich e3 + e4.

Das ist nicht ganz unähnlich ist

erwartet
floor(5/3) + floor(2/3 + 1) 

floor(5/3 + 2/3) + floor(1) 

zu entsprechen, außer Sie von 2^53 multipliziert sind, bevor Sie den Boden nehmen.

Mit 12-Bit-precision floating point und Abschneiden mit Ihren Werten:

 
1.0   = 1.00000000000 
1.1   = 1.00011001100 
1.2   = 1.00110011001 

1.0 + 1.1  = 10.00011001100 // extended during sum 
r1 = 1.0 + 1.1 = 10.0001100110 // truncated to 12 bit 
r1 + 1.2  = 11.01001100101 // extended during sum 
r2 = r1 + 1.2 = 11.0100110010 // truncated to 12 bit 

1.1 + 1.2  = 10.01001100110 // extended during sum 
r3 = 1.1 + 1.2 = 10.0100110011 // truncated to 12 bit 
r3 + 1.0  = 11.01001100110 // extended during sum 
r4 = r3 + 1.0 = 11.0100110011 // truncated to 12 bit 

So die Reihenfolge der Operationen zu ändern/Verkürzungen bewirkt, dass der der Fehler ändern, und r4 = r2!. Wenn Sie 1.1 und 1.2 in diesem System hinzufügen, trägt das letzte Bit, also beim Abschneiden nicht verloren. Wenn Sie 1,0 zu 1,1 hinzufügen, ist das letzte Bit von 1,1 verloren und das Ergebnis ist nicht das gleiche.

In einer Reihenfolge, die Rundung (durch Trunkierung) entfernt eine nachfolgende 1.

In der anderen Reihenfolge, die Rundung entfernt eine nachfolgende 0 beide Male.

Eins ist nicht gleich Null; Die Fehler sind also nicht gleich.

Doubles haben viel mehr Bits an Genauigkeit, und C# verwendet wahrscheinlich eher Runden als Abschneiden, aber hoffentlich zeigt dieses einfache Modell, dass verschiedene Fehler mit unterschiedlichen Ordnungen der gleichen Werte auftreten können.

Der Unterschied zwischen fp und maths ist, dass + eine Abkürzung für 'add then round' ist, anstatt nur add.

+0

+1 gutes konkretes Beispiel – bobince

+0

Das Beispiel ist gut, aber Sie haben die Reihenfolge der Operationen im Vergleich zum ursprünglichen Code geändert. Das OP hat das gleiche beim ersten Schnitt gemacht. –

2

Reihenfolge der Fließkommaoperationen ist wichtig. Antwortet nicht direkt auf Ihre Frage, aber Sie sollten immer vorsichtig sein, Gleitkommazahlen zu vergleichen. Es ist üblich, eine Toleranz enthalten:

double epsilon = 0.0000001; 
if (abs(result1 - result2) <= epsilon) 
{ 
    ... 
} 

Das von Interesse sein könnten: What Every Computer Scientist Should Know About Floating-Point Arithmetic

6

der C# -Compiler ist nichts zu tun. Die CPU ist.

Wenn Sie eine in einem CPU-Registern haben, und man dann B hinzuzufügen, ist das Ergebnis in diesem Register gespeichert ist, A + B, auf die schwimmende Genauigkeit approximiert verwendet

Wenn man dann C hinzuzufügen, fügt der Fehler up . Diese Fehleraddition ist keine transitive Operation, also der letzte Unterschied.

+0

Aber wenn A + B einen Rundungsfehler erzeugt und A + C nicht, hätte (A + C) + B nicht immer den gleichen Rundungsfehler wie A + B? Ich verstehe, dass sich Rundungsfehler addieren, ich frage, warum die Reihenfolge wichtig ist. – d4nt

+0

nein, A + B erzeugt einen Fehler e1, und dann erzeugt A + B + C einen weiteren Fehler e2, so dass der letzte Fehler e1 + e2 ist. Wenn A + C keinen Fehler liefert, liefert A + C + B einen einzelnen Fehler e3, der keinen Grund hat, e1 + e2 zu vergleichen. Sie sollten google für Fließkomma-Arithmetik für weitere Informationen. – Brann

+0

Ich denke, ich mache mich nicht klar, lass mich umformulieren "wenn A + B macht e1, (A + B) + C macht e2, A + C macht e3 und (A + C) + B macht e4, warum tut e1 + e2 nicht gleich e3 + e4? Bis jetzt hast du mir gesagt, dass es nicht ist, aber nicht warum es nicht tut. " – d4nt

0

Sie verwenden eigentlich nicht die gleichen Werte, weil die Zwischenergebnisse unterschiedlich sind:

double result1 = 2.1 + 1.2; 
double result2 = 2.2 + 1.1; 

Da verdoppelt werden nicht Dezimalzahlen darstellen können genau Sie unterschiedliche Ergebnisse erhalten.

4

Siehe the classic paper (What every computer scientist should know about floating-point arithmetic) zu dem Thema. Diese Art von Sachen passiert mit Gleitkommaarithmetik. Es dauert einen Informatiker, Ihnen zu sagen, dass 1/3 + 1/3 + 1/3 gleich 1 is'nt ...

+0

Ich gebe zu, dass Fließkomma-Arithmetik mich erschreckt. Als Faustregel weigere ich mich, es zu benutzen, wenn ich nicht muss. –

+0

Verwenden Sie sie, wenn nützlich: Wo Näherungen in Ordnung sind. Physikalische Simulation, extern gewonnene Messungen, Grafiklayout (für verschiedene Grade der zulässigen Approximation). Geld - nicht so sehr! Es gibt kein Werkzeug, das so einfach ist, dass man sich damit nicht verletzen kann ... (hm, tödliche Pinzettenunfälle?). –

1

Warum die Fehler je nach Auftrag nicht gleich sind, kann an einem anderen Beispiel erläutert werden.

Nehmen wir an, dass es für Zahlen unter 10 alle Nummern speichern kann, so dass es 1, 2, 3 usw. bis einschließlich 10 speichern kann, aber nach 10 kann es nur jede zweite Nummer speichern, mit anderen Worten aufgrund internen Verlust an Präzision, kann es nur 10 speichern, 12, 14, usw.

Jetzt, mit diesem Beispiel sehen Sie, warum die folgende unterschiedliche Ergebnisse:

1 + 1 + 1 + 10 = 12 (or 14, depending on rounding) 
10 + 1 + 1 + 1 = 10 

Das Problem mit Fließkommazahlen besteht darin, dass sie nicht genau dargestellt werden können und der Fehler nicht immer gleich ist. Daher spielt die Reihenfolge eine Rolle.

Zum Beispiel 3,00000000003 + 3,00000000003 kann bis zu sein 6,00000000005 (Anmerkung 6 nicht am Ende) enden, aber 3,00000000003 + 2,99999999997 könnte am 6,00000000001 zu sein, und mit dem Ende:

step 1: 3.00000000003 + 3.00000000003 = 6.00000000005 
step 2: 6.00000000005 + 2.99999999997 = 9.00000000002 

aber, die Reihenfolge ändern :

step 1: 3.00000000003 + 2.99999999997 = 6.00000000001 
step 2: 6.00000000001 + 3.00000000003 = 9.00000000004 

So wird es wichtig sein.

Nun können Sie natürlich Glück haben, dass die obigen Beispiele sich gegenseitig ausgleichen, indem der erste um .xxx1 und der andere um .xxx1 nach oben schwingt, so dass Sie .xxx3 in beiden Fällen haben keine Garantie.

Verwandte Themen