为什么将小浮点数与零进行比较会产生随机结果?

问题描述

我知道浮点数很棘手。但是今天我遇到了一个我无法解释的案例(并且无法使用独立的 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 的效果。它们非常慢(参见 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 == 0)? "=": "!=");

    return 0;
}

输出:

0.000000e+00 = 0
1.681558e-44 != 0

Demo on Godbolt

另见: