16

double我有一种情况,其中一些数值结果(涉及使用和的浮点运算float)对于大输入大小变得不正确,但对于小输入则不正确。

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

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

4

4 回答 4

8

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

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

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

这可能不如 Valgrind 好,因为它更像是一个调试器和手动检查,而不是一个很好的总结,但它确实有效。

于 2013-02-15T14:49:27.507 回答
3

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

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

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

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

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

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

于 2013-02-15T14:59:53.927 回答
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
于 2013-02-15T16:50:32.690 回答
1

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

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

于 2013-02-15T19:09:36.130 回答