Maybe you need to debug an implementation of an algorithm where you may have made a coding mistake and want to trace the floating point computations being carried out. Maybe you need a hook to inspect all values being operated on, looking for values that appear to be out of the range you expect. In C++ you can define your own floating point
class and use operator overloading to write your calculations in a natural way, while retaining the ability to inspect all calculations.
For example, here's a program that defines an FP
class, and prints out all additions and multiplications.
#include <iostream>
struct FP {
double value;
FP( double value ) : value(value) {}
};
std::ostream & operator<< ( std::ostream &o, const FP &x ) { o << x.value; return o; }
FP operator+( const FP & lhs, const FP & rhs ) {
FP sum( lhs.value + rhs.value );
std::cout << "lhs=" << lhs.value << " rhs=" << rhs.value << " sum=" << sum << std::endl;
return sum;
}
FP operator*( const FP & lhs, const FP & rhs ) {
FP product( lhs.value * rhs.value );
std::cout << "lhs=" << lhs.value << " rhs=" << rhs.value << " product=" << product << std::endl;
return product;
}
int main() {
FP x = 2.0;
FP y = 3.0;
std::cout << "answer=" << x + 2 * y << std::endl;
return 0;
}
Which prints
lhs=2 rhs=3 product=6
lhs=2 rhs=6 sum=8
answer=8
Update: I've enhanced the program (on x86) to show the floating point status flags after each floating point operation (only implemented addition and multiplication, others could be easily added).
#include <iostream>
struct MXCSR {
unsigned value;
enum Flags {
IE = 0, // Invalid Operation Flag
DE = 1, // Denormal Flag
ZE = 2, // Divide By Zero Flag
OE = 3, // Overflow Flag
UE = 4, // Underflow Flag
PE = 5, // Precision Flag
};
};
std::ostream & operator<< ( std::ostream &o, const MXCSR &x ) {
if (x.value & (1<<MXCSR::IE)) o << " Invalid";
if (x.value & (1<<MXCSR::DE)) o << " Denormal";
if (x.value & (1<<MXCSR::ZE)) o << " Divide-by-Zero";
if (x.value & (1<<MXCSR::OE)) o << " Overflow";
if (x.value & (1<<MXCSR::UE)) o << " Underflow";
if (x.value & (1<<MXCSR::PE)) o << " Precision";
return o;
}
struct FP {
double value;
FP( double value ) : value(value) {}
};
std::ostream & operator<< ( std::ostream &o, const FP &x ) { o << x.value; return o; }
FP operator+( const FP & lhs, const FP & rhs ) {
FP sum( lhs.value );
MXCSR mxcsr, new_mxcsr;
asm ( "movsd %0, %%xmm0 \n\t"
"addsd %3, %%xmm0 \n\t"
"movsd %%xmm0, %0 \n\t"
"stmxcsr %1 \n\t"
"stmxcsr %2 \n\t"
"andl $0xffffffc0,%2 \n\t"
"ldmxcsr %2 \n\t"
: "=m" (sum.value), "=m" (mxcsr.value), "=m" (new_mxcsr.value)
: "m" (rhs.value)
: "xmm0", "cc" );
std::cout << "lhs=" << lhs.value
<< " rhs=" << rhs.value
<< " sum=" << sum
<< mxcsr
<< std::endl;
return sum;
}
FP operator*( const FP & lhs, const FP & rhs ) {
FP product( lhs.value );
MXCSR mxcsr, new_mxcsr;
asm ( "movsd %0, %%xmm0 \n\t"
"mulsd %3, %%xmm0 \n\t"
"movsd %%xmm0, %0 \n\t"
"stmxcsr %1 \n\t"
"stmxcsr %2 \n\t"
"andl $0xffffffc0,%2 \n\t"
"ldmxcsr %2 \n\t"
: "=m" (product.value), "=m" (mxcsr.value), "=m" (new_mxcsr.value)
: "m" (rhs.value)
: "xmm0", "cc" );
std::cout << "lhs=" << lhs.value
<< " rhs=" << rhs.value
<< " product=" << product
<< mxcsr
<< std::endl;
return product;
}
int main() {
FP x = 2.0;
FP y = 3.9;
std::cout << "answer=" << x + 2.1 * y << std::endl;
std::cout << "answer=" << x + 2 * x << std::endl;
FP z = 1;
for( int i=0; i<310; ++i) {
std::cout << "i=" << i << " z=" << z << std::endl;
z = 10 * z;
}
return 0;
}
The last loop multiplies a number by 10
enough times to show overflow happen. You'll notice precision errors happen as well. It ends with the value being infinity once it overflows.
Here's the tail of the output
lhs=10 rhs=1e+305 product=1e+306 Precision
i=306 z=1e+306
lhs=10 rhs=1e+306 product=1e+307
i=307 z=1e+307
lhs=10 rhs=1e+307 product=1e+308 Precision
i=308 z=1e+308
lhs=10 rhs=1e+308 product=inf Overflow Precision
i=309 z=inf
lhs=10 rhs=inf product=inf