Fragen zu Floating Pointer Over/Underflow, +/-Infinity, Subnormale und NaNs



  • Ich bin auf der Suche nach einer "sicheren" (=erkennt Over/Underflows, Subnormale, NaNs,etc.) und performanten C/C++ Library für einfache Operation wie + - * /...

    Fuer Integer Under/Overflow-Detection nutzen ich die SafeInt Library (https://safeint.codeplex.com/) von Microsoft - nur zu Floats habe ich leider bisher nichts gefunden

    Ich habe mir mal ein Testprogramm geschrieben um die verschiedenen Problemfälle aufzuzeigen - bisher erkenne ich einen Over/Underflow und den Rest nach der Operation - bin mir aber nicht sicher ob das so richtig ist (meine Platformen sind alle IEE754 Konform)

    Hat jemand Erfahrungen mit solchen Tests?

    #include <limits>
    #include <cassert>
    
    static bool is_subnormal_bits(const double& p_value)
    {
      const unsigned int* const p = reinterpret_cast<const unsigned int*>(&p_value);
      return (p[1] & 0x7FF00000) == 0  && (p[0] || p[1] & 0x000FFFFF);
    }
    
    static bool is_pinf(const double& p_value)
    {
      return p_value > 0.0 && p_value/p_value != p_value/p_value;
    }
    
    static bool is_ninf(const double& p_value)
    {
      return p_value < 0.0 && p_value/p_value != p_value/p_value;
    }
    
    static bool is_nan(const double& p_value)
    {
      return p_value != p_value;
    }
    
    int main(int argc, char** argv)
    {
      double max = std::numeric_limits<double>::max(); // groesster double Wert
      double min_before_0 = std::numeric_limits<double>::min(); // kleinster Wert vor 0.0
      double lowest = -max; // kleinster double Wert
      double null = argc - argc; // sonst kommt der Kompiler und mault wegen divison durch 0.0
      assert(null == 0.0);
    
      //NaN
      double nan = sqrt(-1.0);
      assert(is_nan(nan));
    
      //+/- Infinite
      double pinf = 1.0/null;
      double ninf = log(null);
      assert(is_pinf(pinf));
      assert(is_ninf(ninf));
    
      //Subnormal
      double subnormal = min_before_0 / 2.0;
      assert(is_subnormal_bits(subnormal));
    
      //Overflows
    
      //TEST 1
      {
        double a = max;
        double b = 100.0; // ein bisschen ueber max
        double r = a + b;
        assert(r == a); // Ergebnis entspricht immer noch a und b > 0.0
      }
    
      //TEST 2
      {
        double a = max;
        double b = max; // max ueber max
        double r = a + b;
        assert(is_pinf(r)); // +INFINITY
      }
    
      //FRAGE: Ab welchem Wert von b geht das Ergebnis auf +INIFINTY?
    
      //TEST 3
      {
        double a = max;
        double b = 2.0; // max ueber max
        double r = a * b;
        assert(is_pinf(r)); // +INFINITY
      }
    
      // Underflows
    
      //TEST 4
      {
        double a = lowest;
        double b = 100.0;
        double r = a - b;
        assert(r == a); // Ergebnis entspricht immer noch a und b > 0.0
      }
    
      //TEST 5
      {
        double a = lowest;
        double b = max;
        double r = a - b;
        assert(is_ninf(r)); // -INFINITY
      }
    
      //TEST 6
      {
        double a = (lowest + 1.0);
        double b = 2.0;
        double r = a - b;
        assert(r == a); // Ergebnis entspricht immer noch a
      }
    
      //FRAGE: Ab welchem Wert von b geht das Ergebnis auf -INIFINTY?
    
      return 0;
    }
    


  • ich habe jetzt eine checked add und mul routinen gebaut

    bin mir aber einfach nicht sicher ob das alles so richtig ist - oder ob es Lücken gibt die ich nicht sehe, es viel performanter geht, man vor der Operation prüfen sollte/muss/kann

    #include <limits> 
    #include <cassert> 
    
    static bool is_subnormal_bits(const double& p_value) 
    { 
      const unsigned int* const p = reinterpret_cast<const unsigned int*>(&p_value); 
      return (p[1] & 0x7FF00000) == 0  && (p[0] || p[1] & 0x000FFFFF); 
    } 
    
    static bool is_inf(const double& p_value) 
    { 
      return p_value/p_value != p_value/p_value; 
    } 
    
    static bool is_pinf(const double& p_value) 
    { 
      return p_value > 0.0 && is_inf(p_value); 
    } 
    
    static bool is_ninf(const double& p_value) 
    { 
      return p_value < 0.0 && is_inf(p_value); 
    } 
    
    static bool is_nan(const double& p_value) 
    { 
      return p_value != p_value; 
    } 
    
    struct result_t
    {
      enum enum_t
      {
        FP_OK,
        FP_UNDERFLOW,
        FP_OVERFLOW,
        FP_NAN,
        FP_SUBNORMAL
      };
    };
    
    static result_t::enum_t check_result(const double& p_a, const double& p_b, const double& p_result, const double& p_no_change_value)
    {
      if(is_nan(p_result))
      {
        return result_t::FP_NAN;
      }
    
      if(is_subnormal_bits(p_result))
      {
        return result_t::FP_SUBNORMAL;
      }
    
      if(p_b == p_no_change_value) return result_t::FP_OK;
    
      const bool equal = p_a == p_result;
      const bool inf = is_inf(p_result);
      if(equal || inf)
      {
        return (p_b < 0.0) ? result_t::FP_UNDERFLOW : result_t::FP_OVERFLOW;
      }
    
      return result_t::FP_OK;
    }
    
    static double checked_fp_add(const double& p_a, const double& p_b, result_t::enum_t& p_status)
    {
      const double result = p_a + p_b;
      p_status = check_result(p_a, p_b, result, 0.0);
      return result;
    }
    
    static double checked_fp_mul(const double& p_a, const double& p_b, result_t::enum_t& p_status)
    {
      const double result = p_a * p_b;
      p_status = check_result(p_a, p_b, result, 1.0);
      return result;
    }
    
    int main(int argc, char** argv) 
    { 
      double max = std::numeric_limits<double>::max(); // groesster double Wert 
      double min_before_0 = std::numeric_limits<double>::min(); // kleinster Wert vor 0.0 
      double lowest = -max; // kleinster double Wert 
      double null = argc - argc; // sonst kommt der Kompiler und mault wegen divison durch 0.0 
      assert(null == 0.0); 
    
      //NaN 
      double nan = sqrt(-1.0); 
      assert(is_nan(nan)); 
    
      //+/- Infinite 
      double pinf = 1.0/null; 
      double ninf = log(null); 
      assert(is_pinf(pinf)); 
      assert(is_ninf(ninf)); 
    
      //Subnormal 
      double subnormal = min_before_0 / 2.0; 
      assert(is_subnormal_bits(subnormal)); 
    
      //Overflows 
      result_t::enum_t result = result_t::FP_OK;
    
      //TEST 1 
      double r = checked_fp_add(max, 100.0, result);
      assert(result == result_t::FP_OVERFLOW);
    
      //TEST 2 
      r = checked_fp_add(max, max, result);
      assert(result == result_t::FP_OVERFLOW);
    
      //FRAGE: Ab welchem Wert von b geht das Ergebnis auf +INIFINTY? 
    
      //TEST 3 
      r = checked_fp_add(max, 2.0, result);
      assert(result == result_t::FP_OVERFLOW);
    
      //TEST 4 
      r = checked_fp_add(lowest, -100.0, result);
      assert(result == result_t::FP_UNDERFLOW);
    
      //TEST 5 
      r = checked_fp_add(lowest, -max, result);
      assert(result == result_t::FP_UNDERFLOW);
    
      //TEST 6 
      r = checked_fp_add(lowest + 1.0, -2.0, result);
      assert(result == result_t::FP_UNDERFLOW);
    
      //FRAGE: Ab welchem Wert von b geht das Ergebnis auf -INIFINTY? 
    
      //TEST 7
      r = checked_fp_mul(min_before_0, 1.0 / 2.0, result);
      assert(result == result_t::FP_SUBNORMAL);
    
      //TEST 8
      r = checked_fp_mul(max, -2.0, result);
      assert(result == result_t::FP_UNDERFLOW);
    
      //TEST 9
      r = checked_fp_mul(nan, 1.0, result);
      assert(result == result_t::FP_NAN);
    
      return 0; 
    }
    


  • Es geht mir nicht unbedingt darum ob man beim Rechnen sowas beachten muss sondern eher um Abbildung solcher Werte/Bereiche in anderen Software-Tools

    z.B. hatte ich schon mit dem SQL-Server Problem mit Subnormalen und NaNs bei INSERTs - wie es mit INFNITY aussieht habe ich noch nicht getestet



  • warum nutzt Du nicht die Floating Point Exceptions?
    https://msdn.microsoft.com/en-us/library/aa289157(v=vs.71).aspx

    (Abschnitt: Floating-Point Exceptions as C++ Exceptions)



  • warum nutzt Du nicht die Floating Point Exceptions?

    Würde ich (möglicherweise) noch als Erweiterung in meine Library reinbauen - falls schon aktiv gesetzt

    Probleme:

    Ist das Platform-Unabhängig? (IEE754 ist aber schon meine mindest Anforderung)
    hat ARM und andere Platformen was vergleichbares?

    Weil ich damit als Library/Dll Fremd-Applikationen in ihrem Floating-Point Verhalten beeinflusse - falls ich die Exceptions scharf schalten würde
    und es in der Applikation Code gibt der z.B. FP-Exceptions auf C++ Exceptions mappt - aber bisher immer inaktiv war usw. habe ich dann plötzlich mit meiner Lib das Programmverhalten geändert - böse böse

    Oder kann man diese (möglicherweise) applikationsweiten Auswirkungen von FP-Exceptions - heute/anders besser kontrollieren?



  • Abschnitt 3.10 Absatz 10 von einem der letzten C++ Standard Entwürfe sagt

    n4296.pdf schrieb:

    If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:
    […]

    und listet nur Dinge auf, die nichts mit deinen reinterpret_cast s zu tun haben. Das heißt: Dein Code ruft undefiniertes Verhalten hervor. Das würde ich vermeiden.

    Dazu verlässt Du Dich darauf, dass

    • double ein 64bit IEEE754 float ist
    • der Code auf einer little-endian Maschine läuft
    • int 32bit breit ist

    Ich würde stattdessen mit static_assert sicherstellen, dass std::numeric_limits<double>::is_iec559 true ist und statt zwei unsigned int s einfach einen std::uint64_t benutzen — zumindest dann, wenn Du Bitfrickelei bei double s machen willst.

    Und

    p_value/p_value != p_value/p_value
    

    kannst Du natürlich mit

    is_nan(p_value/p_value)
    

    abkürzen.

    Ich frage mich auch, welche Motivation Du hattest, static zu verwenden. Wenn ich nicht möchte, dass etwas außerhalb der Übersetzungseinheit nicht über seinen Namen erreichbar ist, verwende ich dazu in C++ anonyme Namensräume statt static , weil ich es IMHO die Motivation besser ausdrückt.



  • double ein 64bit IEEE754 float ist
    der Code auf einer little-endian Maschine läuft
    int 32bit breit ist

    Das ist mir klar, meine Vorgabe und auch so geprüft - nur eben nicht in diesem Beispiel

    und listet nur Dinge auf, die nichts mit deinen reinterpret_casts zu tun haben.

    ist das ein ganzer Satz - ich habe keine Ahnung was du mir sagen möchtest - was meinst du, float<->int cast aliasing Probleme? das macht der std::uint64_t aber auch nicht besser - und mit 2x uint32 bleibt der Code gleich - ist aber geschmackssache

    Ich frage mich auch, welche Motivation Du hattest, static zu verwenden.

    nur aus Gewohnheit - der VS2010 Kompiler generiert trotz anonymen Namensraum die Funktionen als externals - stört mich irgendwie

    Die Kernfrage ist aber nicht ob mein Code schön ist oder besser aufgebaut werden kann sondern nur ob ich so meine OVERFLOW/UNDERFLOW Tests machen kann



  • Gast3 schrieb:

    und listet nur Dinge auf, die nichts mit deinen reinterpret_casts zu tun haben.

    ist das ein ganzer Satz - ich habe keine Ahnung was du mir sagen möchtest - was meinst du, float<->int cast aliasing Probleme? das macht der std::uint64_t aber auch nicht besser - und mit 2x uint32 bleibt der Code gleich - ist aber geschmackssache

    Du hast nur die zweite Hälfte des Satzes zitiert. Ja. Ich rede vom Aliasing. Ich habe auch nicht behauptet, dass uint64_t das Aliasing-Problem löst. Das war eher ein Tipp bzgl Endianness, weil Du dann die Endian-spezifischen Indizes [0] und [1] nicht mehr brauchst.

    Gast3 schrieb:

    Die Kernfrage ist aber nicht ob mein Code schön ist oder besser aufgebaut werden kann sondern nur ob ich so meine OVERFLOW/UNDERFLOW Tests machen kann.

    Du kannst von mir aus natürlich auch sonst was machen, wie z.B. undefiniertes Verhalten hervorrufen. Aber tatsächlich tun würde ich das an Deiner Stelle nicht.

    Bzgl Underflow: 0.0 oder -0.0 kann auch das Ergebnis eines Underflows sein. Das fängst Du so aber nicht ab.



  • Das war eher ein Tipp bzgl Endianness, weil Du dann die Endian-spezifischen Indizes [0] und [1] nicht mehr brauchst.

    Du kannst von mir aus natürlich auch sonst was machen, wie z.B. undefiniertes Verhalten hervorrufen.

    ok dann nutze ich ein uint64 für die Endianess-Vereinfachung und mach ein memcpy gegen das Aliasing Problem

    die meisten (alle?) Kompiler optimieren das memcpy wenn sie kein Aliasing-Problem sehen auf einen einfachen Cast

    oder hast du einen bessern Tip (Unions sind da ja auch nichts wert)

    Bzgl Underflow: 0.0 oder -0.0 kann auch das Ergebnis eines Underflows sein. Das fängst Du so aber nicht ab.

    kannst du mal ein Beispiel zeigen wo genau die beiden Werte ein Underflow sind?



  • Gast3 schrieb:

    die meisten (alle?) Kompiler optimieren das memcpy wenn sie kein Aliasing-Problem sehen auf einen einfachen Cast

    Hmm, dieser Satz irritiert mich gerade etwas. Verwechselst Du Aliasing vielleicht mit Alignment? Die Fehlerquelle bei der Verletzung der Aliasing-Regel ist doch, dass der Compiler Code nach der as-if-Regel umsortieren und dabei annehmen darf, dass Du die Aliasing-Regeln nicht verletzt. Tust Du es doch, macht das Programm nicht unbedingt mehr das, was es soll.

    Ich hoffe doch mal, dass memcpy von 4 oder 8 Bytes (wobei die Größe zur Übersetzungszeit bekannt ist) irgendwie effizient übersetzt wird. Geprüft habe ich das aber nicht.

    Gast3 schrieb:

    oder hast du einen bessern Tip (Unions sind da ja auch nichts wert)

    Der GCC garantiert übrigens explizit, dass "type punning" per union s OK ist. Es ist quasi ein zusätzliches GCC Feature. Aber aus Gründen der Portabilität würde ich darauf verzichten. Nee, ich habe da jetzt auch keinen besseren Tipp als memcpy parat.

    Gast3 schrieb:

    Bzgl Underflow: 0.0 oder -0.0 kann auch das Ergebnis eines Underflows sein. Das fängst Du so aber nicht ab.

    kannst du mal ein Beispiel zeigen wo genau die beiden Werte ein Underflow sind?

    Klar. Das geht ganz einfach:

    #include <iostream>
    #include <cmath>
    
    double naive_hypot(double x, double y) {
    	return std::sqrt(x*x + y*y);
    }
    
    int main() {
    	double x = 3e-200;
    	double y = 4e-200;
    	double z = naive_hypot(x,y);
    	std::cout << z << std::endl;
    }
    

    In naive_hypot wird x*x und y*y in diesem Beispiel gleich 0.0 wegen Underflow sein. Die Ausgabe des Programms ist also 0 statt 5e-200. Ein Underflow kann Dir also bei zwei Zahlen s und t mit s!=0 und t!=0 für s*t eine 0 geben. Dem Ergebnis allein siehst Du das jetzt nicht an. Eine Multiplikation von zwei Zahlen kann ja wirklich 0 sein, sollte es nur nicht, falls beide Argumente ungleich 0 sind.



  • Hmm, dieser Satz irritiert mich gerade etwas. Verwechselst Du Aliasing vielleicht mit Alignment?

    Ich hoffe doch mal, dass memcpy von 4 oder 8 Bytes (wobei die Größe zur Übersetzungszeit bekannt ist) irgendwie effizient übersetzt wird

    ja verwechselt und ja memcpy wird optimiert (von VStudio, GCC, Clang, ARM, weitere?)

    Klar. Das geht ganz einfach

    wenn man so wie ich auf Einzeloperationen testet muss man den Algorithmus eben auch so schreiben - oder gibt es eine andere Möglichkeit

    double checked_fp_naive_hypot(double x, double y) {
      result_t::enum_t result = result_t::FP_OK;
      double x2 = checked_fp_mul(x,x, result); //==> FP_OVERFLOW
      double y2 = checked_fp_mul(y,y, result); //==> FP_OVERFLOW
      double x2_add_y2 = checked_fp_add(x,y, result);
      double sqrt_result = std::sqrt(x2_add_y2);
      return sqrt_result;
    }
    

    wenn ich einen checked_double-Typ mit Operatoren fuer + - * / oder sowas haette
    dann wuerde dein x*x oder y*y schon eine Exception/oder anderes werfen


Log in to reply