诊断C ++程序中的浮点溢出

时间:2013-02-15 14:19:56

标签: c++ floating-point double overflow numeric

我的情况是,对于大输入大小,某些数值结果(涉及doublefloat的浮点运算)会变得不正确,但对于小输入大小则不然。

一般情况下,我想知道有哪些工具可用于诊断数值溢出和有问题的精度损失等情况。

换句话说:是否有一个工具抱怨溢出等,就像valgrind抱怨内存错误一样?

4 个答案:

答案 0 :(得分:8)

如果启用浮点异常,则FPU可以在溢出时抛出异常。这究竟是如何工作的依赖于操作系统。例如:

  • 在Windows上,您可以使用_control87取消屏蔽_EM_OVERFLOW,以便在溢出时获得C ++异常。
  • 在Linux上,您可以使用feenableexcept在FE_OVERFLOW上启用例外,以便在溢出时获得SIGFPE。例如,要启用所有例外,请在feenableexcept(FE_ALL_EXCEPT)中致电main。要启用溢出并除以零,请调用feenableexcept(FE_OVERFLOW | FE_DIVBYZERO)

请注意,在所有情况下,第三方代码都可能会禁用您已启用的异常;这在实践中可能很少见。

这可能不如Valgrind那么好,因为它更像是一个调试器和手动检查,而不是一个很好的总结,但它的工作原理

答案 1 :(得分:3)

要诊断溢出,可以使用浮点异常。请参阅示例cppreference。请注意,您可能需要使用特定于实现的函数来配置浮点错误的行为。

请注意,即使它们通常被称为“异常”,浮点错误也不会导致C ++异常。

cppreference代码显示了基于IEEE 754的实现的默认行为:您可以在发现适当时检查浮点异常标志。您应该清除输入计算的标志。您可能希望等到计算完成后才能看到,是否已设置任何标志,或者您可能想要检查您怀疑容易出错的每个操作。

可能存在特定于实现的扩展,以使这些“异常”触发您无法忽略的内容。在Windows / MSVC ++上可能是“结构化异常”(不是真正的C ++),在Linux上可能是SIGFPE(所以你需要一个信号处理程序来处理错误)。您将需要特定于实现的库函数甚至编译器/链接器标志来启用此类行为。

我仍然认为溢出不太可能是你的问题。如果您的某些输入变大而其他值仍然很小,则组合它们时可能会丢失精度。控制它的一种方法是使用区间运算。有各种各样的库,包括boost interval

免责声明:我没有使用此库(也没有其他区间运算库)的经验,但也许这可以帮助您入门。

答案 2 :(得分:1)

也许你需要调试一个算法的实现,你可能已经犯了编码错误并希望跟踪正在执行的浮点计算。也许你需要一个钩子来检查所有正在操作的值,寻找看起来超出你期望范围的值。在C ++中,您可以定义自己的floating point类,并使用运算符重载以自然的方式编写计算,同时保留检查所有计算的能力。

例如,这是一个定义FP类的程序,并打印出所有的加法和乘法。

#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;
}

打印

lhs=2 rhs=3 product=6
lhs=2 rhs=6 sum=8
answer=8

更新:我已经增强了程序(在x86上),以便在每个浮点运算后显示浮点状态标志(仅实现加法和乘法,其他可以轻松添加)。

#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;
}

最后一个循环将数字乘以10足够的次数以显示溢出发生。你会发现精确错误也会发生。一旦溢出,它以无限值结束。

这是输出的尾部

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

答案 3 :(得分:1)

除了已发布的优秀建议外,还有另一种方法。编写一个函数来检查浮点数据结构,进行范围和一致性检查。在主循环中插入对它的调用。为了检查其他变量,您可以在检查器发现问题后在其中设置断点。

这比启用异常更多的设置工作,但可以找到更微妙的问题,例如不一致和数字大于预期而没有变得无限,导致检测接近原始问题。