the Compartmented Robust Posix C++ Unit Test system

Chapter 5. Comparing floating point values

Rounding errors makes equality comparisons for floating point values interesting. Usually a comparison with some chosen precision is preferable over a direct equality comparison.

crpcut offers three different ways of making near-equality comparisons for floating point values, each with its own pros and cons.

crpcut::abs_diff

Verify that the difference between two floating point numbers is not greater than a defined maximum. This works well when comparing numbers near zero, but is not very useful otherwise

crpcut::relative_diff

Verify that the quotient between two floating point numbers does not differ by more than a defined maximum. This works very well for large numbers, but is very unsuitable for numbers near zero.

crpcut::ulps_diff

Verify that two floating point numbers do not differ by more than a maximum defined number of possible floating point number representations. This works very well over the entire range of floating point value, but is unfortunately only supported on intel x86 compatibles, and only for 32-bit float and 64-bit double.

All three near-equality comparators are used in the same way, using crpcut::match<matcher>(...) with either ASSERT_PRED(pred, ...) or VERIFY_PRED(pred, ...). An example using crpcut::ulps_diff:


     
     #include <crpcut.hpp>
     #include <numeric>
     #include <limits>
     
     template <typename T>
     class moving_avg
     {
     public:
       moving_avg() : avg(T()), n(T()) {}
       moving_avg& operator+=(T t) { ++n; avg-= (avg - t)/n; return *this; }
       operator T() const { return avg; }
     private:
       T avg;
       T n;
     };
     
     static const unsigned count = 300;
     
     TEST(too_narrow)
     {
       moving_avg<float> mavg;
       float sum = 0.0;
       for (unsigned n = 0; n < count; ++n)
         {
           mavg+= 1.0f/3 + float(n);
           sum+= 1.0f/3 + float(n);
        }
       float avg = sum/count;
       ASSERT_PRED(crpcut::match<crpcut::ulps_diff>(2U), float(mavg), avg);
     }
     
     TEST(close_enough)
     {
       moving_avg<float> mavg;
       float sum = 0.0;
       for (unsigned n = 0; n < count; ++n)
         {
           mavg+= 1.0f/3 + float(n);
           sum+= 1.0f/3 + float(n);
         }
       float avg = sum/count;
       ASSERT_PRED(crpcut::match<crpcut::ulps_diff>(10U), float(mavg), avg);
     }
     
     int main(int argc, char *argv[])
     {
       return crpcut::run(argc, argv);
     }

Running this test program yields:


     FAILED!: too_narrow
     phase="running"  --------------------------------------------------------------
     samples/ulps_diff.cpp:56
     ASSERT_PRED(crpcut::match<crpcut::ulps_diff>(2U), float(mavg), avg)
       param1 = 149.833
       param2 = 149.833
     for crpcut::match<crpcut::ulps_diff>(2U):     Max allowed diff = 2 ULPS
         Actual diff = 5 ULPS
     
     -------------------------------------------------------------------------------
     ===============================================================================
     2 test cases selected
     
                    Sum   Critical   Non-critical
     PASSED   :       1          1              0
     FAILED   :       1          1              0

If you are not confident with the details of floating point calculations, David Goldberg's What Every Computer Scientist Should Know About Floating-Point Arithmetic is a must read. More information about comparing equality between floating point numbers, including a detailed description of the ULPS notion is available from Cygnus in the paper on comparing floating point numbers.

[Caution]Caution
If you use calculations with long double 80-bit floating point numbers, and run in valgrind, your results may disappoint, since valgrind uses 64-bit precision for the 80-bit floating point arithmetic operations.