为什么将一个小的浮点数与零进行比较会产生随机结果?
Why comparing a small floating-point number with zero yields random result?
我知道浮点数很棘手。但是今天我遇到了一个我无法解释的案例(并且无法使用独立的C++代码重现)。
大型项目中的代码如下所示:
int i = 12;
// here goes several function calls passing the value i around,
// but using different types (due to unfortunate legacy code)
...
float f = *((float*)(&i)); // f=1.681558e-44
if (f == 0) {
do something;
} else {
do something else;
}
这段代码会导致随机行为。使用 gdb,确定随机行为是由于比较 f == 0
给出随机结果,即有时为真,有时为假。
代码中的错误是,在使用 f
之前,它应该检查 4 字节是否应该被解释为整数(使用其他辅助信息)。解决方法是先将其转换回整数,然后将整数与0进行比较。然后问题就解决了。
此外,如果需要进行浮点数比较(在这种情况下,浮点数不是如上所示从整数转换而来),我也将比较更改为 abs(f) < std::numeric_limits<float>::epsilon()
,以确保安全.
之后我也想用一个简单的测试程序重现,但是好像重现不了。 (尽管用于该项目的编译器与我用于编译测试程序的编译器不同)。以下是测试程序:
#include <stdio.h>
int main(void){
int i = 12;
float f = *(float*)(&i);
for (int i = 0; i < 5; ++i) {
printf("f=%e %s\n", f, (f == 0)? "=0": "!=0");
}
return 0;
}
我想知道,与零比较的随机行为可能是什么原因?
除了可以轻松修复的未定义行为外,您会看到 denormal numbers. They're extremely slow (see Why does changing 0.1f to 0 slow down performance by 10x?) 的效果,因此在现代 FPU 中通常有非正规数为零 (DAZ) 和刷新为零(FTZ) 标志来控制异常行为。当设置 DAZ 时,非正规化将比较等于零,这就是您观察到的
目前您需要特定于平台的代码来禁用它。这是在 x86 中的实现方式:
#include <string.h>
#include <stdio.h>
#include <pmmintrin.h>
int main(void){
int i = 12;
float f;
memcpy(&f, &i, sizeof i); // avoid UB
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
printf("%e %s 0\n", f, (f == 0)? "=": "!=");
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_OFF);
printf("%e %s 0\n", f, (f == 0)? "=": "!=");
return 0;
}
输出:
0.000000e+00 = 0
1.681558e-44 != 0
另请参阅:
- flush-to-zero behavior in floating-point arithmetic
- Disabling denormal floats at the code level
- Setting the FTZ and DAZ Flags
我知道浮点数很棘手。但是今天我遇到了一个我无法解释的案例(并且无法使用独立的C++代码重现)。
大型项目中的代码如下所示:
int i = 12;
// here goes several function calls passing the value i around,
// but using different types (due to unfortunate legacy code)
...
float f = *((float*)(&i)); // f=1.681558e-44
if (f == 0) {
do something;
} else {
do something else;
}
这段代码会导致随机行为。使用 gdb,确定随机行为是由于比较 f == 0
给出随机结果,即有时为真,有时为假。
代码中的错误是,在使用 f
之前,它应该检查 4 字节是否应该被解释为整数(使用其他辅助信息)。解决方法是先将其转换回整数,然后将整数与0进行比较。然后问题就解决了。
此外,如果需要进行浮点数比较(在这种情况下,浮点数不是如上所示从整数转换而来),我也将比较更改为 abs(f) < std::numeric_limits<float>::epsilon()
,以确保安全.
之后我也想用一个简单的测试程序重现,但是好像重现不了。 (尽管用于该项目的编译器与我用于编译测试程序的编译器不同)。以下是测试程序:
#include <stdio.h>
int main(void){
int i = 12;
float f = *(float*)(&i);
for (int i = 0; i < 5; ++i) {
printf("f=%e %s\n", f, (f == 0)? "=0": "!=0");
}
return 0;
}
我想知道,与零比较的随机行为可能是什么原因?
除了可以轻松修复的未定义行为外,您会看到 denormal numbers. They're extremely slow (see Why does changing 0.1f to 0 slow down performance by 10x?) 的效果,因此在现代 FPU 中通常有非正规数为零 (DAZ) 和刷新为零(FTZ) 标志来控制异常行为。当设置 DAZ 时,非正规化将比较等于零,这就是您观察到的
目前您需要特定于平台的代码来禁用它。这是在 x86 中的实现方式:
#include <string.h>
#include <stdio.h>
#include <pmmintrin.h>
int main(void){
int i = 12;
float f;
memcpy(&f, &i, sizeof i); // avoid UB
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON);
printf("%e %s 0\n", f, (f == 0)? "=": "!=");
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_OFF);
printf("%e %s 0\n", f, (f == 0)? "=": "!=");
return 0;
}
输出:
0.000000e+00 = 0
1.681558e-44 != 0
另请参阅:
- flush-to-zero behavior in floating-point arithmetic
- Disabling denormal floats at the code level
- Setting the FTZ and DAZ Flags