[gelöst] Problem mit 7-stelligen Float



  • Hallo,

    ich bin noch Anfänger in der C++-Programmierung und versuche mir nach und nach mehr anzueignen.

    Mein Problem:
    Ich will ein 3D-Array mit float Werten erzeugen (Zeile 20), es befüllen (Zeile 26) als Ergebnis erhalte ich jedoch nur Integer (Ausgabe von Zeile 33).

    Mein Code:

    #include <iostream>
    
    using namespace std;
    
    int main()
    {
      // spatial dimensions
      const int size = 20;
      const float grid_length = 1.0f; // [meter]
    
      // physical parameter
      const float T_s = 293.0f;  // Surface temperature [K] // EDIT
      const float p0 = 101300.0f;    // Surface pressure [Pa]
    
      // physical constants
      const float R = 287.058f;  // gas constant [J / (K * kg) ]
      const float rho_0 = p0 / (R * T_s);    // Density of air [kg / m³]
      const float g = 9.81f;   // gravity acceleration [m / s²]
    
      // generate arrays of pressure
      float p[size][size][size];
    
      // initialize the temperature and pressure:
      for (unsigned int x = 0; x < size; x++){
          for (unsigned int y = 0; y < size; y++){
               for (unsigned int z = 0; z < size; z++){
                   p[x][y][z] = p0 - rho_0 * g * grid_length * static_cast<float> (z);
               }
          }
      }
    
      // Output
      for (unsigned int i=0; i < size; i++){
          std::cout << p[0][0][i] << "\n";
      }
    }
    

    Die Ausgabe enthält überraschender Weise jedoch keine Floats, sondern nur noch Integer (sie wird quasi nach der 6. Stelle abgeschnitten):

    101300
    101288
    101276
    101265
    ...
    

    Mein System / Compiler:
    Ubuntu 14.04 / g++ Version 4.8.2

    Meine eigene Einschätzung war zuerst, dass ich eventuell einen Speicherüberlauf habe oder einen Casting-Fehler mache, jedoch müssten doch normale floats für diesen Wertebereich ausreichen, oder? Mit Google ließ sich nichts finden, da es dort meist nur um das gewollte "Abschneiden" von Nachkommazahlen ging.

    Was mache ich falsch?

    // EDIT: Deklaration von T_s eingefügt, um Beispiel kompilierfähig zu machen



  • Was ist T_s?
    Welches Ergebnis erwartest du?


  • Mod

    Das Standardausgabeformat fuer Fliesskommazahlen sind Sechs signifikante Stellen.

    Moechtest du mehr, dann ersetze Zeile 33 durch

    std::cout << setprecision(8) << p[0][0][i] << "\n";
    

    Das gibt dir bspw. Acht signifikante Stellen. Aber Vorsicht: floats sind natuerlich nicht praezise, und in der Groessenordnung von einer Million wird es schnell viele Annaeherungs-Ungenauigkeiten geben, wodurch die Nachkommastellen unbrauchbar werden.

    Aber hoere lieber nicht auf mich, ich habe von Fliesskommazahlen wenig Ahnung... 🤡

    P.S.: Wer das Beispiel kompilieren will, muss in Zeile 16 den Initializer zu 1.225 aendern.



  • Vielen Dank!
    Mit setprecision(8) und der Einbindung von "<iomanip>" funktioniert es! 🙂

    Tut mir leid, beim Kopieren ist mir eine Zeile entfallen, habe es nachträglich editiert.



  • "Funktioniert es" - Was funktioniert jetzt und was ging vorher nicht? Was ist denn jetzt anders? Hast du verstanden was passiert ist? Gibt es vllt einen Grund dass standartmäßig auf sechs stellen (bei der Darstellung der Zahl) gerundet wird?
    http://ideone.com/WIiGgM

    Ist jetzt eigentlich irgendwas anders an dem Programm?

    Fragen über Fragen.



  • Ich hatte nicht verstanden warum die Ausgabe nur 6 Stellen anzeigt und hatte an einen Casting- oder Überlauffehler gedacht. Arcoth hat mir aber freundlicherweise gezeigt, dass es nur an dem Standardausgabeformat lag, welches man mithilfe von setprecision() aus der Klasse "<iomanip>" ändern kann. Dementsprechend "funktioniert es" jetzt.

    Letztendlich hat sich mit dem Befehl an dem Programm und den berechneten Werten ja nichts geändert, sondern nur die Ausgabe. Warum die Standardausgabe nur 6 Stellen ausgibt, weiß ich nicht. Definitionssache? 😉



  • Ich versuch mal, das ganze mit ein bisschen mehr Details zu füllen. Das grundsätzliche Problem im Umgang mit reellen Zahlen ist, dass es sehr viel mehr reelle Zahlen gibt, als auch der größte Computer speichern könnte, und dass selbst das noch mehr sehr, sehr viel mehr ist, als man mit einem Computer sinnvoll handhaben kann. Es gibt zwei Arten, mit der Misere umzugehen -- fixed point und floating point.

    Fixed Point (nur kurz umrissen) bedeutet, dass man mit Brüchen rechnet, die alle den gleichen Nenner haben. Das bietet sich beispielsweise für bestimmte Anwendungen an, die mit Geldbeträgen arbeiten, indem man halt alles als Hundertstel auffasst.

    Floating Point (das, was du benutzt) bedeutet, dass man praktisch mit wissenschaftlicher Notation arbeitet. Dezimal schreibt man zum Beispiel

    1.234567e6 = 1.234567 * 106

    um 1234567 darzustellen. "Floating Point" heißt das deshalb, weil die Stelle des Dezimalpunktes in der wissenschaftlichen Darstellung je nach Exponent in der ausgeschriebenen Darstellung hin- und herschwebt. Dabei nennt man die 1.234567 die Mantisse und die 6 den Exponenten.

    Da der Speicherplatz begrenzt ist, ist diese Rechenweise nur mit bestimmten Einschränkungen durchführbar -- die Länge der Mantisse und des Exponenten ist begrenzt. Ich postuliere als Beispiel mal einen dezimalen Fließkommatyp mit einer siebenstelligen Mantisse und einem zweistelligen Exponenten. Das bedeutet, die (betragsmäßig) größte Zahl, die der Typ darstellen kann, ist

    9.999999e99

    und die kleinste ist

    0.000001e-99

    (Ich ignoriere hier ein paar Eigenheiten der üblichen Standards -- die Behandlung von Vorzeichen und subnormalen Zahlen -- um die Erklärung einfach zu halten. Nur, falls hier jemand mitliest, der das alles auch im Detail kennt und sich über meinen Mangel an Pedanterie aufregt.)

    Sofort ergeben sich eine ganze Reihe Probleme:

    1. Genauigkeit

    Da die Mantisse begrenzt ist, können Zahlen mit vielen signifikanten Stellen nicht genau dargestellt werden. So passen zum Beispiel 1.2345678, 123.45678 und 12345678 nicht in den Typen hinein. Ihre Darstellung wäre

    1.2345678e0
    1.2345678e2
    1.2345678e6

    ...aber 8 Stellen Mantisse haben wir halt nicht zum Speichern, und so würde gerundet. Dabei käme dann

    1.234568e0
    1.234568e2
    1.234568e6

    als tatsächlicher Wert heraus, also 1.234568, 123.4568 und 12345680.

    2. Die üblichen Rechengesetze gelten nicht mehr.

    Beispiel: Mit echten reellen Zahlen ist 1234567 + 0.1 = 1234567.1, und 1234567.1 ist mit unserem Typen nicht darstellbar. Es ergibt sich eine Situation, in der y != 0 und x + y == x.

    Beispiel Assoziativität: (0.4 + 0.4) + 1234567 = 0.4 + (0.4 + 1234567) <=> 1234568 = 1234567

    Auf ähnliche Weise werden alle Rechenarten beschädigt. Man spricht hier von Rundungsfehlern, die sich daraus ergeben, dass Zwischenergebnisse und Ergebnisse von Berechnungen nicht genau speicherbar sind. Häufig kommt man dabei in der ungefähren Umgebung des tatsächlichen Ergebnisses raus, aber es gibt durchaus Zusammenhänge, in denen das nicht der Fall ist. Zum Beispiel

    3. Vergleiche

    Ist (1 / 3) * 3 == 1?

    Nun:

    1 / 3 = 3.333333e-1 = 0.3333333
    (1 / 3) * 3 = 0.3333333 * 3 = 0.9999999

    ...nein. Man behilft sich daher in der Regel mit Epsilon-Vergleichen. x ist etwa gleich y, wenn |x - y| < ε, wobei ε eine passend gewählte, kleine Zahl ist (gerne von x oder y abhängig, etwa |x * 10-6|).

    4. Auslöschung

    Worin sich in der Differenz aus zwei berechneten, ungenauen Werten der noch signifikante Anteil weghebt und nur noch der Rundungsfehler übrig bleibt. Beispiel:

    (1234567 + 0.5) - (1234567 + 0.499) = 1234568 - 1234567 = 1

    Als wäre all das noch nicht genug, rechnen Computer binär statt dezimal. Das ist im Grunde nicht weiter schlimm -- mathematisch sind die beiden Näherungen etwa gleich gut -- es führt aber dazu, dass die meisten Zahlen, die wir als Menschen der Maschine vorwerfen, von dieser genausowenig genau dargestellt werden können wie 1/3 in dezimaler Notation. Gleichgültig, wie groß ein binärer Fließkommatyp ist, 0.1 wird er nie genau darstellen können.

    Soweit die Theorie. Was bedeutet das für deinen Code? Nun, von den kleineren Rundungsfehlern mal abgesehen, hast du in

    p0 - rho_0 * g * grid_length * static_cast<float> (z)
    

    eine Differenz zwischen zwei Zahlen in doch recht unterschiedlichen Größenordnungen -- sie unterscheiden sich anscheinend so um Faktor 10000, also säbelst du dir vier dezimale, signifikante Stellen schonmal weg. Dummerweise hat ein handelsüblicher float nach ieee-754 nur 7.22 dezimale, signifikante Stellen (24 binäre), so dass dir noch gut drei übrigbleiben. Wenn du jetzt eine Zahl in der Größenordnung von 105 auf 8 Nachkommastellen ausgeben lässt, die 7 signifikante Stellen hat, von denen 3 aus deiner Rechnung stammen, wie verlässlich wird wohl das Ergebnis sein?

    Wenn du jetzt all deine floats in doubles umwandelst, sieht die Lage deutlich weniger angespannt aus -- der hat 53 binäre Stellen, was umgerechnet etwa 16 dezimale sind. Ich lege dir also dringend ans Herz, das zu tun.



  • Wow, Danke erstmal für deinen ausführlichen Kommentar 🙂

    Ich hatte zwar schon von der Ungenauigkeit der Floats gehört, aber eigentlich gedacht, dass es bei meinen kleinen Zahlen noch nicht wirklich beachtenswert ist. Aber du hast natürlich recht, ich habe eine relativ große Zahl, deren geringen Abweichung ich berechnen will. Wenn ich nun so große Rundungsfehler in Kauf nehme, kann ich die Zahlen auch gleich würfeln.

    Ich werde deine Antwort beherzigen, nochmal vielen Dank!

    Noch zu 3) Gibt es in C++ Module oder Klassen, die diese Vergleichsoperatoren (also ungefähr gleich, innerhalb der Epsilon-Umgebung) bereitstellen? Also quasi in Pseudocode:

    A = 2.5
    B = 2.4
    Mein_Epsilon = 0.1
    if(is_equal(A,B)){
        cout << "Stimmt schon, Pi mal Daumen...";
    }
    

  • Mod

    Ist (1 / 3) * 3 == 1?

    Nun:

    1 / 3 = 3.333333e-1 = 0.3333333
    (1 / 3) * 3 = 0.3333333 * 3 = 0.9999999

    ...nein

    1/3 wird bei float, welches eine Mantisse von 23 Bit hat, folgendermaßen dargestellt: M = 2796203 (am nähesten dran an 2796202232796202\frac{2}{3}), der Exponent e ist -2.
    Die eigentliche Mantisse ist 43\frac{4}{3}. Aber die Darstellung der Mantisse ist nicht präzise, und damit wird die Mantisse als m=1+279620328m = 1 + \frac{2796203}{2^8} dargestellt, was etwa 1.3333333731.333333373 ist; das Endergebnis ist
    (1+2796203223)0.250,3333333433(1 + \frac{2796203}{2^{23}}) * 0.25 \approx 0,3333333433
    Was auch genau das Ergebnis von

    std::cout << std::setprecision(10) << 1/3.f << '\n';
    

    ist.

    ...nein

    Das hängt wiederum davon ab, wie dargestellt wird. Auf meiner Maschine zum Beispiel ist das mit 3.f statt 3 (Was im Übrigen auch 3. bzw. 3.0 sein sollte) wahr. Weil eben das Dreifache von 1/3.0 oder 1/3.0f am genauesten durch 1 dargestellt wird.



  • Ich habe von einem hypothetischen Dezimalfloat her argumentiert, bei dem das definiert ist.


Anmelden zum Antworten