【问题标题】:gcc compiler optimization influences result of floating point comparisongcc编译器优化影响浮点比较的结果
【发布时间】:2020-03-03 02:34:36
【问题描述】:

问题

在使用自动化 CI 测试时,我发现了一些代码,如果 gcc 的优化设置为 -O2,则会中断。如果双精度值在任一方向越过阈值,代码应增加计数器。

解决方法

转到 -O1 或使用 -ffloat-store 选项可以解决此问题。

示例

这是一个显示相同问题的小示例。 只要*pNextState * 1e-6 的序列超过阈值 0.03,update() 函数就应该返回 true。 我使用引用调用,因为这些值是完整代码中大型结构的一部分。

使用<>= 背后的想法是,如果一个序列恰好命中该值,则函数这次应该返回1,下一个循环返回0。

test.h:

extern int update(double * pState, double * pNextState);

test.c:

#include "test.h"

int update(double * pState, double * pNextState_scaled) {
    static double threshold = 0.03;
    double oldState = *pState;
    *pState = *pNextState_scaled * 1e-6;

    return oldState < threshold && *pState >= threshold;
}

main.c:

#include <stdio.h>
#include <stdlib.h>

#include "test.h"

int main(void) {

    double state = 0.01;
    double nextState1 = 20000.0;
    double nextState2 = 30000.0;
    double nextState3 = 40000.0;

    printf("%d\n", update(&state, &nextState1));
    printf("%d\n", update(&state, &nextState2));
    printf("%d\n", update(&state, &nextState3));

    return EXIT_SUCCESS;
}

使用至少 -O2 的 gcc 输出是:

0
0
0

将 gcc 与 -O1、-O0 或 -ffloat-store 一起使用会产生所需的输出

0
1
0

据我了解,如果编译器优化堆栈上的局部变量 oldstate 并与精度更高(80 位)的浮点寄存器中的中间结果进行比较,并且值 *pState 是比阈值小一点。 如果用于比较的值以 64 位精度存储,则逻辑不会错过阈值。由于乘以 1e-6,结果可能存储在浮点寄存器中。

你会认为这是一个 gcc 错误吗? clang 没有显示问题。

我在 Intel Core i5、Windows 和 msys2 上使用 gcc 版本 9.2.0。

更新

我很清楚浮点比较并不精确,我认为以下结果是有效的:

0
0
1

想法是,如果(*pState &gt;= threshold) == false 在一个周期内,那么在随后的调用(*pState &lt; threshold) 中将相同的值 (oldstate = *pState) 与相同的阈值进行比较必须为真。

【问题讨论】:

  • 不相关,但为什么要传递pNextState_scaled 参数的指针?
  • FWIW,x86 的 gcc 4.8.5 在所有情况下都会产生预期的结果。
  • 我认为这是gcc bug 323,其中 gcc 使用超额精度是标准未批准的地方。另请参阅gcc faq on the subject
  • @AProgrammer 即使存在编译器错误,代码仍然过度依赖精确的浮点相等,这完全是在自找麻烦
  • @SteveFriedl,在某些情况下浮点相等是合理的,但不是很好,尽管引入了额外的 fudge 因素使其不合理,尤其是当它与这里的不等式结合时。

标签: c gcc floating-point compiler-optimization


【解决方案1】:

[免责声明:这是一个通用的,从臀部射击的答案。浮点问题可能很微妙,我没有仔细分析过这个问题。偶尔,像这样看起来可疑的代码 毕竟可以移植且可靠地工作,并且根据接受的答案,这里似乎就是这种情况。尽管如此,通用答案仍然适用于一般情况。]

我认为这是测试用例中的错误,而不是 gcc。这听起来像是一个典型的代码示例,在精确的浮点相等方面不必要地脆弱。

我会推荐:

  • 重写测试用例,或者也许
  • 删除测试用例。

我会推荐:

  • 通过切换编译器来解决这个问题
  • 通过使用不同的优化级别或其他编译器选项来解决它
  • 提交编译器错误报告[虽然在这种情况下,它似乎存在一个编译器错误,尽管它不需要提交,因为它已经提交了]

【讨论】:

    【解决方案2】:

    我分析了你的代码,我认为它符合标准,但你被gcc bug 323 击中,你可以在gcc FAQ 中找到更多可访问的信息。

    在存在 gcc 错误的情况下修改函数并使其健壮的一种方法是存储先前状态低于阈值的事实,而不是(或另外)存储该状态。像这样的:

    int update(int* pWasBelow, double* pNextState_scaled) {
        static double const threshold = 0.03;
        double const nextState = *pNextState_scaled * 1e-6;
        int const wasBelow = *pWasBelow;
        *pWasBelow = nextState < threshold;
        return wasBelow && !*pWasBelow;
    }
    

    请注意,这并不能保证可复制性。您可能会在一种设置中获得0 1 0,在另一种设置中获得0 0 1,但您迟早会检测到转换。

    【讨论】:

    • 我非常喜欢这个,但我仍然想知道真正的近距离通话。一个尼特和一个花絮。 Nit:int const wasBelow = *pWasBelow; 缺少 * ptr 取消引用,并且花絮简化了对 return wasBelow != *pWasBelow; 的返回
    • @SteveFriedl,修复了取消引用。我没有更改花絮,因为它会检测两个方向的阈值传递,而不是向上。
    • 我发现了研究中提到的gcc bug,但不确定是不是一样。错误报告比较了相同计算的结果,但我的示例确实比较了相同的值,只是存储在一个中间变量中。然而,过度精确的问题可能是一样的。
    【解决方案3】:

    我将其作为答案,因为我认为我不能在评论中编写真正的代码,但 @SteveSummit 应该得到赞誉 - 如果没有他们上面的评论,我不可能找到这个。

    一般建议是:不要与浮点值进行精确比较,这似乎就是这样做的。如果计算值几乎完全是0.03,但由于内部表示或优化,它会稍微偏离而不是完全,那么它看起来就像一个阈值交叉。

    因此,人们可以通过添加一个 epsilon 来解决这个问题,该 epsilon 表示一个人与阈值的接近程度,而无需考虑跨越它。

    int update(double * pState, double * pNextState_scaled) {
        static const double threshold    = 0.03;
        static const double close_enough = 0.0000001f; // or whatever
        double oldState = *pState;
        *pState = *pNextState_scaled * 1e-6;
    
        // if either value is too close to the threshold, it's not a crossing
        if (fabs(oldState - threshold) < close_enough) return 0;
        if (fabs(*pState  - threshold) < close_enough) return 0;
    
        return oldState < threshold && *pState >= threshold;
    }
    

    我想你必须了解你的应用程序才能知道如何适当地调整这个值,但是一个比你要比较的值小几个数量级的耦合似乎在附近。

    【讨论】:

    • 这允许在没有检测到事件的情况下跨越阈值。对于 OP 来说,这可能比检测到交叉点的精确时刻的变化更糟糕。
    • @AProgrammer 我不相信它允许越过阈值而不被检测到,因为我们已经定义达到 0.0299999999 或 0.0300000001(或其他值)太接近而无法计数作为一个交叉点,但如果 OP 的应用程序即将达到阈值,则需要一些不同的逻辑。
    • @AProgrammer 我想我想这个,你可能是对的。如果它是 0.0300000001,那么它肯定已经从下方穿过,尽管我们可以将 0.029999999 也定义为已经穿过。这取决于 OP 的应用程序,但不做直接浮点相等的一般概念仍然主要是指导原则。
    猜你喜欢
    • 1970-01-01
    • 2020-04-14
    • 2012-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-10
    相关资源
    最近更新 更多