GNU C++中程序使用浮点数的奇怪行为
Strange behavior of program in GNU C++, using floating-point numbers
看看这个程序:
#include <iostream>
#include <cmath>
using namespace std;
typedef pair<int, int> coords;
double dist(coords a, coords b)
{
return sqrt((a.first - b.first) * (a.first - b.first) +
(a.second - b.second) * (a.second - b.second));
}
int main()
{
coords A = make_pair(1, 0);
coords B = make_pair(0, 1);
coords C = make_pair(-1, 0);
coords D = make_pair(0, -1);
cerr.precision(20);
cerr << dist(A, B) + dist(C, D) << endl;
cerr << dist(A, D) + dist(B, C) << endl;
if(dist(A, B) + dist(C, D) > dist(A, D) + dist(B, C))
{
cerr << "*" << endl;
}
return 0;
}
函数 dist 返回两点之间的距离。A、B、C、D 是正方形的角。
它应该是 dist(A, B) == dist(B, C) == dist(C, D) ==dist(D, A) == sqrt(2)。
and dist(A, B) + dist(C, D) == dist(A, D) + dist(B, C) == 2 * sqrt(2)
我正在使用GNU/Linux,i586,GCC 4.8.2。
我编译这个程序并运行:
$ g++ test.cpp ; ./a.out
2.8284271247461902909
2.8284271247461902909
*
我们看到,程序输出相等的 dist(A, B
) + dist(C, D) 和 dist(A, D) + dist(B, C),但条件 dist(A, B) + dist(C, D)> dist(A, D) + dist(B, C) 是真的!当我使用 -O2 编译时,它看起来还可以:
$ g++ test.cpp -O2 ; ./a.out
2.8284271247461902909
2.8284271247461902909
我认为,这是一个 gcc-bug,它与浮点运算精度没有直接关系,因为在这种情况下,值 dist(A, B) + dist(C, D) 和 dist(A, D) + dist(B, C) 必须相等(它们中的每一个都是 sqrt(2) + sqrt(2))。
当我更改函数 dist 时:
double dist(coords a, coords b)
{
double x = sqrt((a.first - b.first) * (a.first - b.first) + (a.second - b.second) * (a.second - b.second));
return x;
}
程序运行正确。所以问题不在于数字的浮点表示,而在于 gcc 代码。
编辑:
32 位编译器的简化示例:
#include <iostream>
#include <cmath>
using namespace std;
int main()
{
if (sqrt(2) != sqrt(2))
{
cout << "Unequal" << endl;
}
else
{
cout << "Equal" << endl;
}
return 0;
}
在不优化的情况下运行:
$ g++ test.cpp ; ./a.out
Unequal
使用 -ffloat-store 运行:
$ g++ test.cpp -ffloat-store ; ./a.out
Equal
溶液:
可能,它不是 GCC #323 中的"错误":https://gcc.gnu.org/bugzilla/show_bug.cgi?id=323
使用 -ffloat-store 编译可以解决问题。
这种看似奇怪的行为是由于旧的 x87 浮点单元的工作方式:它使用具有 64 位精度的 80 位long double
类型作为其寄存器格式,而临时 double
的长度为 64 位,精度为 53 位。 发生的情况是编译器将 sqrt(2)
结果中的 1 个溢出到内存(因为 sqrt
返回一个double
,这四舍五入到该类型的 53 位有效数),以便 FP 寄存器堆栈对于下一次调用 sqrt(2)
是明确的。 然后,它将从内存加载的 53 位精度值与从其他sqrt(2)
调用返回的未舍入的 64 位精度值进行比较,它们出现不同,因为它们的舍入方式不同,正如您从此汇编器输出中看到的那样(注释我的,使用了您的第二个代码片段,为了清晰起见,将 2s 更改为 2.0s,-Wall -O0 -m32 -mfpmath=387 -march=i586 -fno-builtin
用于 Godbolt 上的编译标志):
main:
# Prologue
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
# Argument push (2.0)
subl $8, %esp
movl $0, %eax
movl $1073741824, %edx
pushl %edx
pushl %eax
# sqrt(2.0)
call sqrt
# Return value spill
addl $16, %esp
fstpl -16(%ebp)
# Argument push (2.0)
subl $8, %esp
movl $0, %eax
movl $1073741824, %edx
pushl %edx
pushl %eax
# sqrt(2.0)
call sqrt
addl $16, %esp
# Comparison -- see how one arg is loaded from a spill slot while the other is
# coming from the ST(0) i387 register
fldl -16(%ebp)
fucompp
fnstsw %ax
# Status word interpretation
andb $69, %ah
xorb $64, %ah
setne %al
testb %al, %al
# The branch was here, but it and the code below was snipped for brevity's sake
结论:x87是怪人。 -mfpmath=sse
是这种行为的最终修复 -- 它将使 GCC 将 FLT_EVAL_METHOD
定义为 0, 因为 SSE(2) 浮点支持只有单/双。 -ffloat-store
开关也适用于此程序,但不建议将其作为通用解决方法 - 由于额外的溢出/填充,它会使您的程序变慢,并且并非在所有情况下都有效。 当然,使用 64 位 CPU/OS/编译器组合也可以解决此问题,因为 x86-64 ABI 默认使用 SSE2 进行浮点数学运算。
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 如何修复此错误:未定义对"距离(浮点数,浮点数,浮点数,浮点数,浮点数)"的引用
- C++浮点数据类型和字符串数据类型无法子到模板函数中
- 使用提升将数据从 PyObject 复制到浮点数 *
- 使用浮点数和双精度数的非常小数字的数学
- 使用英特尔内联函数将打包的 8 位整数乘以浮点数向量
- 如何在 c++ 中将小数点后两位数的浮点数分配给另一个浮点数
- 返回浮点数的小数位数
- txt 文件中浮点数的最大和最小值
- 为什么 std::cout 打印浮点数、双精度和长双精度到相同的小数精度?
- 将浮点数转换为无符号字符数组并打印出来
- 如何将时间字符串 (M:SS) 转换为浮点数
- 如何将浮点数(*)[6]转换为浮点**类型?
- C++将浮点数乘以分数
- 如何将浮点数转换为uint8_t?
- 将字符串转换为浮点数或整数,而无需使用内置函数(如 atoi 或 atof)
- 如何在C++(Arduino)中将浮点数组转换为字节数组
- 在数学上将浮点数四舍五入到 N 位小数
- Cython通过浮点数的最快方式,用于高频控制回路
- 复制 -nan 表示浮点数,AVX __m256 复制后显示 0