Rechenprogramm liefert "machmal" falsche Werte


  • Mod

    Nimm mal an, ein float hätte nur 3 Bit. Dann kann ich damit 8 Werte darstellen. Beispielsweise 0/8, 1/8, 2/8, 3/8, ... oder in Dezimalschreibweise: 0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875. Das ist effektiv weniger als eine Dezimalstelle an Genauigkeit, denn ich könnte dir 0, 0.1, 0.2, 0.3, 0.5, 0.6, 0.7, oder 0.8 sagen, und du wüsstest jeweils exakt, welche der darstellbaren Zahlen ich damit meine (und 0.4 und 0.9 fehlen sogar). Trotzdem hat die vollständige Darstellung dieser Zahlen im Dezimalsystem bis zu 3 Nachkommastellen. Diesbezüglich ist es egal, dass man nur eine zählende Stelle hat (bzw. sogar ein bisschen weniger), die Darstellung interessiert sich dafür nicht.

    Dabei ist der Transfer vom Binärsystem ins Dezimalsystem noch halbwegs gutartig. Angenommen der float würde intern nicht binär sondern mit einem 3-wertigen Tri-Bit arbeiten. Dann hätte ein float mit 2 solcher Tri-Bits 9 mögliche Zustände 0/9, 1/9, 2/9, und so weiter. Das sind effektiv immer noch weniger als eine Dezimalstelle, aber wenn man die Zahlen im Dezimalsystem vollständig ausschreiben wollte, bräuchte jeder dieser Werte (außer 0) unendlich viele Stellen!

    Genauigkeit != Anzahl der Stellen der Darstellung

    Dass man alle Binärzahlen im Dezimalsystem exakt darstellen kann, liegt übrigens an den Primfaktoren. Die Basis des Binärsystems (2) ist vollständig in der Basis des Dezimalsystems (10=2*5) enthalten. Die Basis des Dreiersystems (3) hingegen gar nicht, daher sind alle Werte des Dreiersystems in Dezimalschreibweise so krumm (außer die, wo sich die 3 wegkürzt, also die Ganzzahlen). Und auch umgekehrt von Dezimal zu Binär fehlt die 5, daher ist 0.1 = 1/(2*5) nicht exakt im Binärsystem darstellbar (Häufiger Fallstrick beim Programmieren! Initialisiert man Fließkommawerte mit 0.1 oder ähnlichem, dann ist das gar nicht genau 0.1!), sondern nur so Werte wie 0.5 = 5/(2*5) = 1/2 oder 0.75 = 75 / (2*5)^2 = 3/2^2, wo sich die 5 heraus kürzt.



  • @SeppJ Also, habs mir nochma angeguckt. Deine Darlegungen sind zwar absolut richtig, erklären aber das Phänomen unzureichend. Der springende Punkt ist, daß float tatsächlich zuwenig signifikante Dezimale abbildet.
    Wenn man statt der scanf()- Einleserei a und b entsprechend vorbelegt, hat man nur noch konstante Ausdrücke, da erledigt der Compiler die Rechnerei vorab in double- precision. Keine Abweichung.
    Legt man hs als double an, ist der Spuk ebenfalls aus der Bude gebannt, double reicht aus. Auch keine Abweichung.
    Als float angelegt, wird bei live- Werten hs zu unpräzise errechnet, da aber bei printf für %f der Zieltyp tatsächlich double ist, haben wir für die eigentliche Operation zwei unterschiedliche Zieltypen, für hs eben float (erst für die Ausgabe in double gewandelt), für printf direkt ist es halt schon double. Bedeutet Abweichung.

    War das jetzt zu faselig oder kammas verstehn? Was auch noch recht interessant sein könnte, nachdem die Bitbreiten in C/++ relativ unverbindlich sind, gibt es wenigstens für float/double was Verbindliches? Hab da wenig Ahnung.

    PS: Eigentlich hätte ich mich an eine Workshopreihe an der Uni erinnern müssen, einer der erstaunlichsten war "In der Floating- Point- Falle"



  • @Sarkast sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Wenn man statt der scanf()- Einleserei a und b entsprechend vorbelegt, hat man nur noch konstante Ausdrücke, da erledigt der Compiler die Rechnerei vorab in double- precision. Keine Abweichung.

    Das ist völlig wurscht. Ob das jetzt zur Compiletime passiert oder zur Runtime. In dem ganzen Ausdruck ist mindestens ein double drin, also wird auch mit double gerechnet.

    Sonst genau das was ich hier schon gesagt habe. [expr.call/12] und [conv.fpprom] (schimpft sich default argument promotion).

    @Sarkast sagte in Rechenprogramm liefert "machmal" falsche Werte:

    gibt es wenigstens für float/double was Verbindliches?

    Von IEEE 754 (IEC 559) kannst Du ausgehen. Soll sogar angeblich bald vom Standart vorgeschrieben werden (P1467R1).
    std::numeric_limits<>::is_iec559() verrät es Dir.

    Früher mal haben x86 CPUs in 80 bit gerechnet (das waren long double), heute sind es 32 (float) und 64 (double) bit.

    Der Standart verlang von long double nur daß er mindestens so breit ist wie double ([basic.fundamental]/12).

    // final edit: done.



  • @Swordfish sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Das ist völlig wurscht. Ob das jetzt zur Compiletime passiert oder zur Runtime. In dem ganzen Ausdruck ist mindestens ein double drin, also wird auch mit double gerechnet.

    Nee, ehmts ja nicht. Hab ein wenig Spielwiese gemacht:

    #define GETVALUES
    // #define MAKEDOUBLE
    int main(int argc, char *argv[])
    {
    //    printf("Hello, world!\n");
    int a, b;
    #ifdef MAKEDOUBLE
    double hs;
    #else
    float hs;
    #endif
    #ifdef GETVALUES
    printf("Give a: ");
    scanf("%i", &a);
    #else
       a = 26; printf(" %i\n",a);
    #endif
    
    #ifdef GETVALUES
    printf("Give b: ");
    scanf("%i", &b);
    #else
       b = 27; printf(" %i\n",b);
    #endif
    hs = a * b * 365.25 * 24 * 60;
    printf("Ergebnis HS: %f\n", hs);
    printf("Ergebnis aus Printf: %f\n", a * b * 365.25 * 24 * 60);
    return 0;
    }
    

    Das hab ich gemeint, float wird nur bei der Runtime wirksam, auch wenn's für printf nachher in double gewandelt wird.

    @Swordfish sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Sonst genau das was ich hier schon gesagt habe. [expr.call/12] und [conv.fpprom] (schimpft sich default argument promotion).

    @Sarkast sagte in Rechenprogramm liefert "machmal" falsche Werte:

    gibt es wenigstens für float/double was Verbindliches?

    Der Standart verlang von long double nur daß er mindestens so breit ist wie double ([basic.fundamental]/12).

    // final edit: done.
    Ah, OK, das wußte ich noch nicht! 👍



  • @Sarkast sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Das hab ich gemeint, float wird nur bei der Runtime wirksam, auch wenn's für printf nachher in double gewandelt wird.

    Keine Ahnung was Du sagen willst.

    hs   =   a * b * 365.25 * 24 * 60  ;
    //   ^   ^^^^^^^^^^^^^^^^^^^^^^^^
    //   |   DER Ausdruck ist double, egal ob a und b mit scanf() gelesen werden oder nicht und auch egal welchen Typ hs hat.
    //   +--- da findet eine Konvertierung von double zu float statt wenn hs float ist (= mist)
    

    Das ist so. Egal ob das zur Compiletime oder zur Runtime gerechnet wird.



  • @Sarkast hs kann halt nur die ungenaueren float-Darstellung aufnehmen.
    Daher geht da die Information verloren.
    Die kommt auch nicht wieder, wenn bei printf in double gewandelt wird.



  • Es könnte sein dass bestimmte Compiler mit bestimmten Einstellungen (fast math und so) darauf "verzichten" die Werte auf das was float halten kann zusammenzustauchen. Könnte evtl. auch den Effekt erklären dass mit scanf was anderes rauskommt als mit Literalen.



  • @hustbaer sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Es könnte sein dass bestimmte Compiler mit bestimmten Einstellungen (fast math und so) darauf "verzichten" die Werte auf das was float halten kann zusammenzustauchen. Könnte evtl. auch den Effekt erklären dass mit scanf was anderes rauskommt als mit Literalen.

    Danke, genau das wollte ich klarmachen, deswegen habe ich die zwei kleinen conditionals reingemogelt. Einfach mal GETVALUES auskommentieren und float hat Null Bedeutung.



  • @Sarkast sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Einfach mal GETVALUES auskommentieren und float hat Null Bedeutung.

    Wir sind hier bei Standart-C++. Und da stimmt eben Deine Behauptung eben einfach nicht.


  • Mod

    @Swordfish sagte in Rechenprogramm liefert "machmal" falsche Werte:

    @Sarkast sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Einfach mal GETVALUES auskommentieren und float hat Null Bedeutung.

    Wir sind hier bei Standart-C++. Und da stimmt eben Deine Behauptung eben einfach nicht.

    Das beobachtete Verhalten ist perfekt standardkonform.



  • @SeppJ sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Das beobachtete Verhalten ist perfekt standardkonform.

    Auf welchen Post beziehst Du Dich?

    Ich habe von der Behauptung gesprochen daß es einen Unterschied machen würde ob irgendwas zur Compiletime oder zur Runtime berechnet wird oder woher die Werte dazu kommen (user input vs. initializer).


  • Mod

    @Swordfish sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Ich habe von der Behauptung gesprochen daß es einen Unterschied machen würde ob irgendwas zur Compiletime oder zur Runtime berechnet wird oder woher die Werte dazu kommen (user input vs. initializer).

    Auf das hier. Aber ich muss mich selber korrigieren. In C++11 wurde es geändert, so dass laut Standard nun Casts und Zuweisungen zu einer Präzisionsänderung führen müssen. Diese Änderung war zu obskur, als dass ich sie seit 2011 jemals mitbekommen habe. Praktisch hält sich ja auch gar kein Compiler an diese Vorgabe, außer man zwingt ihn mittels Compilerschaltern dazu.

    Siehe
    https://en.cppreference.com/w/cpp/types/climits/FLT_EVAL_METHOD
    und
    https://en.cppreference.com/w/cpp/preprocessor/impl



  • @SeppJ sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Praktisch hält sich ja auch gar kein Compiler an diese Vorgabe

    Du möchtest mir sagen, daß es Compiler gibt (nein, nicht mit irgendwelchen Switches) bei denen in float foo = 0.42 * 21; das foo plötzlich double precision hat!?


  • Mod

    @Swordfish sagte in Rechenprogramm liefert "machmal" falsche Werte:

    Du möchtest mir sagen, daß es Compiler gibt (nein, nicht mit irgendwelchen Switches) bei denen in float foo = 0.42 * 21; das foo plötzlich double precision hat!?

    Jain. Ich möchte dir das sagen, was dir auch Sarkast und hustbaer sagen möchten, aber da du denen nicht zuhörst, versuche ich es auch nicht weiter.



  • @SeppJ Ich geh' nochmal lesen.

    @SeppJ sagte in Rechenprogramm liefert "machmal" falsche Werte:

    aber da du denen nicht zuhörst

    Wie kommst Du darauf? Vielleicht die Möglichkeit in Betracht gezogen, daß ich da wirklich etwas nicht verstehe?


  • Mod

    Ich bin mir nicht sicher, ob Sarkast das richtige meint, je öfter ich es lese, desto eher denke ich, das nicht. Aber das was hustbaer sagte gilt: Wenn Compiler Ausdrücke zur Compilerzeit auswerten, werden sie dabei oft unendliche Präzision benutzen. Auch wenn das nicht strikt standardkonform ist. Dadurch kann was anderes rauskommen, je nachdem, ob man einen Ausdruck mit Compilezeitkonstanten hat, oder ob man die Werte zur Laufzeit einliest. Dummerweise gelingt es mir gerade nicht, ein gutes Beispiel zu produzieren, weil ich nicht unendlich Zeit habe 🙂



  • @SeppJ Darf der Compiler

    int main(int argc, char *argv[])
    {
    int a = 26, b =27;
    float hs;
    
    hs = a * b * 365.25 * 24 * 60;
    printf("Ergebnis HS: %f\n", hs);
    // printf("Ergebnis aus Printf: %f\n", a * b * 365.25 * 24 * 60);
    return 0;
    }
    

    durch

    int main(int argc, char *argv[])
    { puts("369223936.000000");
    }
    

    ersetzen ?
    oder schreibt er 369223920.000000 (den double Wert)?


  • Mod

    edit: dieses Beispiel ist falsch.

    Vielleicht nicht das beste Beispiel, aber mal fix von Stackoverflow inspiriert:

    #include <stdio.h>
    
    int main()
    {
      double d = 1234567890*9876543210*123098456876;
      printf("%f\n", d);
    
      int a,b,c;
      scanf("%d %d %d", &a, &b, &c);
      double d2 = a*b*c;
      printf("%f\n", d2);
    }
    

    Gibt bei mir unterschiedliche Ergebnisse.



  • @SeppJ sagte in Rechenprogramm liefert "machmal" falsche Werte:

    9876543210

    passt auch nicht mehr in 32-Bit.


  • Mod

    @DirkB sagte in Rechenprogramm liefert "machmal" falsche Werte:

    @SeppJ Darf der Compiler

    int main(int argc, char *argv[])
    {
    int a = 26, b =27;
    float hs;
    
    hs = a * b * 365.25 * 24 * 60;
    printf("Ergebnis HS: %f\n", hs);
    // printf("Ergebnis aus Printf: %f\n", a * b * 365.25 * 24 * 60);
    return 0;
    }
    

    durch

    int main(int argc, char *argv[])
    { puts("369223936.000000");
    }
    

    ersetzen ?
    oder schreibt er 369223920.000000 (den double Wert)?

    Ehrlich gesagt, weiß ich nach der ganzen Standardleserei gerade nicht mehr so recht. Ich würde sagen: Vor C++11, ja, nach C++11, nein (oder genauer: C99 war's, auf das sich C++11 bezieht).

    @DirkB sagte in Rechenprogramm liefert "machmal" falsche Werte:

    @SeppJ sagte in Rechenprogramm liefert "machmal" falsche Werte:

    9876543210

    passt auch nicht mehr in 32-Bit.

    Mist. Ich ziehe dieses spezielle Beispiel zurück. Es gibt aber irgendwelche Kombinationen, wo da ein Unterschied heraus kommt. Das ist der Grund, wieso der GCC die Abhängigkeit von der GMP hat, damit er das machen kann. Vielleicht haben sie es auch mittlerweile angepasst, so dass der GCC da strikter dem Standard folgt. Ich teste solche Dinge schließlich nicht routinemäßig alle paar Jahre. Jedenfalls gab es da mindestens in der prä-C++11 und prä-SSE Ära Kombinationen, wo unerwartete Ergebnisse rauskamen, sowohl wegen 80Bit 387-Registern, als auch wegen Compiletimeauswertungen.


Anmelden zum Antworten