【问题标题】:Why comparing a small floating-point number with zero yields random result?为什么将小浮点数与零进行比较会产生随机结果?
【发布时间】:2021-06-15 08:32:34
【问题描述】:

我知道浮点数很棘手。但是今天我遇到了一个我无法解释的案例(并且无法使用独立的 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;
}

我想知道,与零比较的随机行为可能是什么原因?

【问题讨论】:

  • 这隐藏了编译器关于带有强制类型转换的指针类型的警告,但它仍然违反了严格的别名规则:取消引用为不兼容类型的对象别名的指针是未定义的行为。
  • @bruin 不,比这更糟。严格的别名规则允许像void foo(int * a, float * b) 这样的函数假设对*a 的更改不会更改*b。它允许编译器假定*(float*)(&amp;i); 只出现在无法访问的代码中,并消除整个程序。
  • 您要求我们推测未定义的行为。这是一项毫无意义的任务。根据定义,未定义行为不受逻辑分析。请发minimal reproducible example 重现问题且不包含UB。
  • UB 类型的双关语不谈,你有一个不正常的float。这些数字的行为可能取决于编译器标志、舍入模式以及谁知道还有什么。
  • @bruin std::memcpy 在这种情况下会给出明确定义的行为。 "...当需要将对象的字节解释为不同类型的值时,可以使用 std::memcpy 或 std::bit_cast (C++20 起):..." en.cppreference.com/w/cpp/language/reinterpret_cast 请注意,这种情况下的 c-stype 转换等价于 reinterpret_cast

标签: c++ floating-point denormal-numbers


【解决方案1】:

除非可以轻松修复未定义的行为,否则您将看到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, (f == 0)? "=": "!=");

    return 0;
}

输出:

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

Demo on Godbolt

另见:

【讨论】:

  • 非常感谢。现在我可以确定确切的 UB 代码行并重现该错误。那行代码是*dst = *(float*)(&amp;tmp),即取消引用实际上指向整数的浮点指针(正如@Caleth 指出的那样)。如果我将这些 UB 代码更改为 memcpy(),问题就消失了。再次感谢!
  • Re “in x86”:x86 是架构的昵称,而不是平台。 macOS、Linux 等系统之间的代码可能不同,并且它们的版本之间以及所使用的开发者工具之间可能会有所不同。
猜你喜欢
  • 2013-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-14
相关资源
最近更新 更多