gcc精度错误漏洞?

8
我只能假设这是一种错误。第一个断言通过了,而第二个没有通过:
double sum_1 =  4.0 + 6.3;
assert(sum_1 == 4.0 + 6.3);

double t1 = 4.0, t2 = 6.3;

double sum_2 =  t1 + t2;
assert(sum_2 == t1 + t2);

如果不是一个 bug,那是什么原因呢?

您还可以查看以下链接:- https://dev59.com/SUXRa4cB1Zd3GeqPoQVe - https://dev59.com/P3VD5IYBdhLWcg3wU5-H - https://dev59.com/I0fRa4cB1Zd3GeqP6Rov - pingw33n
7个回答

12

你正在比较浮点数。不要这样做,因为浮点数在某些情况下存在固有的精度误差。相反,获取两个值的差的绝对值,并断言该值小于一些小数字(epsilon)。

void CompareFloats( double d1, double d2, double epsilon )
{
    assert( abs( d1 - d2 ) < epsilon );
} 

这与编译器无关,而与浮点数实现方式有关。以下是IEEE规范:

http://www.eecs.berkeley.edu/~wkahan/ieee754status/IEEE754.PDF


4
“精度误差”是浮点数算术的特性,这是无法避免的,它不是gcc编写者粗心实现的错误,而是计算机在处理分数时的方式。建议搜索“计算机科学家应了解的浮点数算术知识”,并将其内容牢记于心。 - High Performance Mark
是的,我已经添加了一个到规范的链接。 - Ed S.

12
这是我也遇到过的问题。
浮点数不应该进行等式比较,因为存在舍入误差,你可能已经知道这一点。但在这种情况下,你计算了t1+t2,然后又重新计算了一次。那么这肯定会产生相同的结果,对吧?
这里发生了什么事情。我敢打赌你是在x86 CPU上运行的,对吗?x86 FPU使用80位内部寄存器,但在内存中存储值时使用64位double。所以t1+t2首先使用80位精度进行计算,然后——我假设——以64位精度存储在sum_2中,并且进行了一些舍入。对于断言,它被加载回浮点寄存器,并且再次使用80位精度计算t1+t2。因此,现在您正在将曾经舍入为64位浮点值的sum_2与使用更高精度(80位)计算的t1+t2进行比较,这就是为什么值不完全相同的原因。
编辑:那么为什么第一个测试通过呢?在这种情况下,编译器可能在编译时计算4.0+6.3并将其存储为64位数量,同时用于赋值和断言。因此,正在比较相同的值,而断言通过了。
第二次编辑:这是为代码的第二部分生成的汇编代码(gcc,x86),带有注释——基本上遵循上述情况。
// t1 = 4.0
fldl    LC3
fstpl   -16(%ebp)

// t2 = 6.3
fldl    LC4
fstpl   -24(%ebp)

// sum_2 =  t1+t2
fldl    -16(%ebp)
faddl   -24(%ebp)
fstpl   -32(%ebp)

// Compute t1+t2 again
fldl    -16(%ebp)
faddl   -24(%ebp)

// Load sum_2 from memory and compare
fldl    -32(%ebp)
fxch    %st(1)
fucompp

有趣的一点是:这是在没有优化的情况下编译的。当使用-O3进行编译时,编译器会优化所有代码。


我认为那还不是这样的。问题在于4.0 + 6.3是一个表达式,由编译器常量折叠为10.3。因此,第一个assert等同于assert(10.3 == 10.3),它会轻松通过。在第二个测试中,它实际上将4.0放入double中,将6.3放入double中(失去了一点精度),将它们相加,并将其与常量10.3进行比较,由于它们之间的差异约为2^-70,所以失败了。 :) - hobbs
我不太确定...如果编译器可以将t1+t2优化为10.3并在assert语句中执行,为什么它不能在对sum_2进行赋值时执行同样的优化呢? - Martin B
看了代码清单,我明白了。 :) 我猜错了,我的确是。 - hobbs
感谢您提供如此精彩的答案!您说得对——我正在运行x86,我确实知道通常不应该比较浮点数,但这是在一个单元测试中,以确保某些矩阵操作正常工作。当使用Visual Studio编译时,它可以通过……唉。 - Jesse Elliott
这正是我遇到这种情况的情形——在一个单元测试中,我认为我可以要求完全相等。 - Martin B

3

我已经在我的Intel Core 2 Duo上复制了您的问题,并查看了汇编代码。 这是发生的情况:当您的编译器评估t1 + t2时,它会执行以下操作:

load t1 into an 80-bit register
load t2 into an 80-bit register
compute the 80-bit sum

当它存储到 sum_2 中时,它会这样做。
round the 80-bit sum to a 64-bit number and store it

然后,==比较将80位的总和与64位的总和进行比较,它们是不同的,主要是因为小数部分0.3不能使用二进制浮点数精确表示,所以您正在比较一个已被截断为两个不同长度的“重复小数”(实际上是重复二进制数)。
真正令人恼火的是,如果使用gcc -O1gcc -O2编译器编译,gcc在编译时会出错,但问题会消失。也许这符合标准,但这只是gcc不是我最喜欢的编译器的又一个原因。
附言:当我说 == 将80位总和与64位总和进行比较时,当然我真正意思是它将64位总和的扩展版本进行比较。您可能需要好好思考一下。
sum_2 == t1 + t2

解析为

extend(sum_2) == extend(t1) + extend(t2)

并且

sum_2 = t1 + t2

解析为

sum_2 = round(extend(t1) + extend(t2))

欢迎来到浮点数的奇妙世界!

3

当比较浮点数的接近程度时,通常希望衡量它们的相对差异,其定义为

if (abs(x) != 0 || abs(y) != 0) 
   rel_diff (x, y) = abs((x - y) / max(abs(x),abs(y))
else
   rel_diff(x,y) = max(abs(x),abs(y))

例如,
rel_diff(1.12345, 1.12367)   = 0.000195787019
rel_diff(112345.0, 112367.0) = 0.000195787019
rel_diff(112345E100, 112367E100) = 0.000195787019

这个想法是测量数字共有的领先有效数字的数量;如果你对0.000195787019取-log10,你会得到3.70821611,这大约是所有示例共同具有的领先基数10位数的数量。

如果您需要确定两个浮点数是否相等,应该执行以下操作:

if (rel_diff(x,y) < error_factor * machine_epsilon()) then
  print "equal\n";

机器精度是指浮点数硬件中可以保存的最小数字。大多数计算机语言都有一个函数调用来获取这个值。error_factor 应该基于您认为在计算数字 x 和 y 的舍入误差(和其他误差)中将被消耗的有效数字的数量。例如,如果我知道 x 和 y 大约是1000个总和的结果,并且不知道任何总和的数字边界,我会将 error_factor 设置为大约100。
尝试将它们作为链接添加,但由于这是我的第一篇帖子,所以无法添加链接:
相对差异:en.wikipedia.org/wiki/Relative_difference 机器精度:en.wikipedia.org/wiki/Machine_epsilon
有效数字(尾数):en.wikipedia.org/wiki/Significand
舍入误差:en.wikipedia.org/wiki/Rounding_error

2

也许在其中一个情况下,您会将64位双精度浮点数与80位内部寄存器进行比较。查看GCC为这两种情况发出的汇编指令可能很有启发性...


1

双精度数的比较本质上是不准确的。例如,你经常会发现0.0 == 0.0返回false。这是由于FPU存储和跟踪数字的方式。

维基百科说

测试相等性是有问题的。两个在数学上相等的计算序列可能会产生不同的浮点值。

您需要使用一个delta来给出比较的容差,而不是一个精确的值。


0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接