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

Diagnosis of floating-point overflows in C++ programs

本文关键字:溢出 诊断 程序 C++      更新时间:2023-10-16

我遇到这样一种情况,即一些数值结果(涉及doublefloat的浮点运算)对于大的输入大小变得不正确,但对于小的输入大小却不正确。

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

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

如果启用浮点异常,则FPU可以在溢出时抛出异常。具体操作方式取决于操作系统。例如:

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

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

这可能不如Valgrind好,因为它更像是一个调试程序和手动检查,而不是一个get-a-nice-summary-at-the-end,但它是有效的。

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

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

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

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

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

免责声明:我没有这个库(也没有其他区间算术库)的经验,但也许这可以让你开始。

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

这更多的是设置工作,而不是启用异常,但可能会遇到更微妙的问题,如不一致和数字比预期的要大,而不会无限大,从而导致检测接近原始问题。

也许您需要调试一个算法的实现,因为您可能犯了编码错误,并希望跟踪正在执行的浮点计算。也许您需要一个钩子来检查正在操作的所有值,寻找超出您预期范围的值。在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 << "answerswer=" << 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 nt"
          "addsd %3, %%xmm0 nt"
          "movsd %%xmm0, %0 nt"
          "stmxcsr %1 nt"
          "stmxcsr %2 nt"
          "andl  $0xffffffc0,%2 nt"
          "ldmxcsr %2 nt"
          : "=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 nt"
          "mulsd %3, %%xmm0 nt"
          "movsd %%xmm0, %0 nt"
          "stmxcsr %1 nt"
          "stmxcsr %2 nt"
          "andl  $0xffffffc0,%2 nt"
          "ldmxcsr %2 nt"
          : "=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 << "answerswer=" << x + 2.1 * y << std::endl;
    std::cout << "answerswer=" << 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