gcc 编译器优化影响浮点比较的结果
gcc compiler optimization influences result of floating point comparison
问题
在使用自动化 CI 测试时,我发现了一些代码,如果将 gcc 优化设置为 -O2,这些代码就会中断。如果 double 值在任一方向超过阈值,代码应递增计数器。
解决方法
使用 -O1 或使用 -ffloat-store 选项可以解决该问题。
例子
这是一个小例子,它显示了同样的问题。
只要 *pNextState * 1e-6
的序列超过 0.03 的阈值,update()
函数就应该 return 为真。
我使用了引用调用,因为这些值是完整代码中大型结构的一部分。
使用 <
和 >=
的想法是,如果一个序列恰好命中该值,则该函数这次应该 return 1,下一次应该 return 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 位)和值 [=20 的浮点寄存器中的中间结果进行比较,则会出现问题=] 比阈值小一点点。
如果比较值以 64 位精度存储,则逻辑不会错过超过阈值的情况。由于乘以 1e-6 结果可能存储在浮点寄存器中。
您认为这是一个 gcc 错误吗?
clang 没有显示问题。
我在 Intel Core i5、Windows 和 msys2 上使用 gcc 9.2.0 版。
更新
我很清楚浮点数比较不准确,我认为以下结果有效:
0
0
1
这个想法是,如果在一个周期中 (*pState >= threshold) == false
,那么在后续调用 (*pState < threshold)
中将相同的值 (oldstate = *pState
) 与相同的阈值进行比较必须为真。
[免责声明:这是一个通用的、草率的答案。浮点数问题比较微妙,我没有仔细分析过这个。偶尔,像这样看起来可疑的代码 can 毕竟可以移植和可靠地工作,并且根据接受的答案,这里似乎就是这种情况。然而,通用答案适用于一般情况。]
我会认为这是测试用例中的错误,而不是 gcc 中的错误。这听起来像是一个典型的代码示例,在精确的浮点相等方面不必要地脆弱。
我会推荐:
- 重写测试用例,或者
- 正在删除测试用例。
我不会推荐:
- 通过切换编译器来解决这个问题
- 通过使用不同的优化级别或其他编译器选项来解决这个问题
- 提交编译器错误报告[尽管在这种情况下,它出现在那里是一个编译器错误,尽管它已经提交了,所以不需要提交]
我将此作为答案,因为我认为我无法在评论中编写真正的代码,但 @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;
}
我想您必须了解您的应用程序才能知道如何适当地调整此值,但附近似乎有一个比您要比较的值小几个数量级的耦合。
我已经分析了您的代码,我认为它符合标准,但您遇到了问题 gcc bug 323 about which you may find more accessible information in 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
,但您迟早会发现这种转变。
问题
在使用自动化 CI 测试时,我发现了一些代码,如果将 gcc 优化设置为 -O2,这些代码就会中断。如果 double 值在任一方向超过阈值,代码应递增计数器。
解决方法
使用 -O1 或使用 -ffloat-store 选项可以解决该问题。
例子
这是一个小例子,它显示了同样的问题。
只要 *pNextState * 1e-6
的序列超过 0.03 的阈值,update()
函数就应该 return 为真。
我使用了引用调用,因为这些值是完整代码中大型结构的一部分。
使用 <
和 >=
的想法是,如果一个序列恰好命中该值,则该函数这次应该 return 1,下一次应该 return 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 位)和值 [=20 的浮点寄存器中的中间结果进行比较,则会出现问题=] 比阈值小一点点。 如果比较值以 64 位精度存储,则逻辑不会错过超过阈值的情况。由于乘以 1e-6 结果可能存储在浮点寄存器中。
您认为这是一个 gcc 错误吗? clang 没有显示问题。
我在 Intel Core i5、Windows 和 msys2 上使用 gcc 9.2.0 版。
更新
我很清楚浮点数比较不准确,我认为以下结果有效:
0
0
1
这个想法是,如果在一个周期中 (*pState >= threshold) == false
,那么在后续调用 (*pState < threshold)
中将相同的值 (oldstate = *pState
) 与相同的阈值进行比较必须为真。
[免责声明:这是一个通用的、草率的答案。浮点数问题比较微妙,我没有仔细分析过这个。偶尔,像这样看起来可疑的代码 can 毕竟可以移植和可靠地工作,并且根据接受的答案,这里似乎就是这种情况。然而,通用答案适用于一般情况。]
我会认为这是测试用例中的错误,而不是 gcc 中的错误。这听起来像是一个典型的代码示例,在精确的浮点相等方面不必要地脆弱。
我会推荐:
- 重写测试用例,或者
- 正在删除测试用例。
我不会推荐:
- 通过切换编译器来解决这个问题
- 通过使用不同的优化级别或其他编译器选项来解决这个问题
- 提交编译器错误报告[尽管在这种情况下,它出现在那里是一个编译器错误,尽管它已经提交了,所以不需要提交]
我将此作为答案,因为我认为我无法在评论中编写真正的代码,但 @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;
}
我想您必须了解您的应用程序才能知道如何适当地调整此值,但附近似乎有一个比您要比较的值小几个数量级的耦合。
我已经分析了您的代码,我认为它符合标准,但您遇到了问题 gcc bug 323 about which you may find more accessible information in 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
,但您迟早会发现这种转变。