有没有一种方法可以减轻舍入误差?

Is there one way to alleviate roundoff errors?

提问人:zg c 提问时间:7/18/2023 最后编辑:zg c 更新时间:7/18/2023 访问量:88

问:

关于“保护位”的维基百科提供了一个示例代码:

#include <stdio.h>
int main(){
   double a;
   int i;

   a = 0.2; 
   a += 0.1; 
   a -= 0.3;

   for (i = 0; a < 1.0; i++) 
       a += a;

   printf("i=%d, a=%f\n", i, a);

   return 0;
}

使用我的zen2 r7 4800h cpu,我通过编译了上述源代码。然后它输出与维基百科相同。Guard_digit.cgcc Guard_digit.c -std=c17 -march=znver2 -pedantic -O0 -o With_Guard_digit.oi=54, a=1.000000

正如本注释所说,IEEE标准已经实现了保护数字:

IEEE 标准要求使用 3 个意义较小的额外位 比单精度中隐含的 24 位(尾数)要多 表示法。

尾数格式加上额外的位:

1.XXXXXXXXXXXXXXXXXXXXXXX   0   0   0                                                                                                                                          

^         ^                 ^   ^   ^
|         |                 |   |   |
|         |                 |   |   -  sticky bit (s)
|         |                 |   -  round bit (r)
|         |                 -  guard bit (g)
|         -  23 bit mantissa from a representation
-  hidden bit

问:有没有一种方法可以通过更改源代码或其他代码来解决这个精度和舍入问题(即误差偏移可以在一定程度上减轻,以便它可以输出类似的东西)?i=108, a=1.000000

查看 Eric Postpischil 的答案后编辑:

很抱歉没有清楚地描述问题。我想知道如何通过保留原始计算来解决舍入问题,因此不考虑直接计算。a = 0;

我想解决这个具体问题,但不是一般的。正如评论所说,这超出了我目前的范围。

c 浮点 精度

评论

6赞 Some programmer dude 7/18/2023
遇到了什么问题?请尽量使您的问题自成一体,这样我们就不需要去外部链接(可能会消失,或者其内容发生变化)来了解您的问题。
0赞 zg c 7/18/2023
感谢您的快速回复。我更新了问题描述。
1赞 Aconcagua 7/18/2023
long double常量需要再迭代一次,超出常量在 Godbolt 上允许的处理时间......不过,我还是认为该程序不会无休止地运行,只是将错误累积到达到 1.0 花费的时间太长了。doublelong double
1赞 chux - Reinstate Monica 7/18/2023
@zg c,而不是 ,用于更清楚地看到正在发生的事情。printf("i=%d, a=%f\n", i, a);printf("i=%d, a=%f %a\n", i, a, a);
1赞 Simon Goater 7/18/2023
明智的做法是将所有浮点值视为近似值。正如这个例子所显示的,通常人们甚至无法确定结果中的百分比误差。虽然 a 和 b 可能都具有 24 位精度,但 a+b 可能是 100% 噪声。如果需要精确性,可以使用整数。有些语言支持有理数作为数据类型,但在 c 语言中,你必须用整数来处理它们。另一种解决方案是将基数 10 用于浮点数和双精度数,尽管现代计算机没有对这些类型的硬件支持。gcc.gnu.org/onlinedocs/gcc-4.5.4/gcc/Decimal-Float.html

答:

3赞 Eric Postpischil 7/18/2023 #1

问:有没有一种方法可以通过更改源代码或其他代码来解决这个精度和舍入问题(即误差偏移可以在一定程度上减轻,以便它可以输出类似的东西)?i=108, a=1.000000

在常见的 C 实现中,不可能通过添加和/或减去 .0625 或更高的值来生成,这将导致所示循环在迭代超过 57 次后终止。a

这是因为常见的 C 实现使用 IEEE-754 binary64(也称为“双精度”)表示 ,而 binary64 使用 53 位有效位。这意味着 binade 中以 .0625 开头的值用一个有效位表示,其高位的位置值为 2−4 (.0625),其低位的位置值为 2−56(跨越 53 位,包括两个端点)。double

加法和减法可以将位带到高位,就像小学算术中教授的那样,但永远不会在最低输入位置以下产生非零位。因此,通过添加和减去大于或等于 .0625 的值生成的任何结果都不能有任何低于 2−56 的非零位。

因此,在执行此类算术后进入循环时,我们有以下情况之一:

  • a为负数或零,并且循环永不终止。
  • a是 2−56 或更大,迭代 57 次或更少将使其大于 1。

有没有一种方法可以通过更改源代码来解决这个精度和舍入问题......

显然,0.2 + 0.1 - 0.3 的正确结果可以通过将源代码从以下位置更改为:

a = 0.2; 
a += 0.1; 
a -= 0.3;

自:

a = 0;

这是计算中的一个常见问题:你不能通过问“我如何得到这些值的解决方案?”来正式描述你想要解决的一般问题,因为这样就有一个简单的解决方案,它只是这些值的一个答案,它对你没有普遍的帮助。

相反,您必须描述整个问题类别。例如,您可以问:“如何编写代码来查找最多 30 个正负十进制数字的精确十进制和,小数点后最多三位数字,小数点前最多两位数?

进一步注意,你不希望在另一个方向上走得太远,使问题完全笼统而不是完全具体。如果问题是在没有错误的情况下添加和减去任何十进制数字,则必须编写任意精度算术。如果问题是用一些适度数量的数字相加和相减一些适度数量的十进制数字,那么这个问题可以通过使用算术和精心选择的舍入来解决。具体的解决方案可能取决于您选择的参数。因此,您需要很好地描述问题的特征。double

评论

0赞 zg c 7/18/2023
感谢您的详细回答。1. 我理解你引用的“binade”,$2^{-56}$ 与迭代时间有关,但你为什么要强调这个数字“0.0625”?2. 我会尝试更新我的问题。我正在学习David A. Patterson和John L. Hennessy的一本基础教科书“计算机组织与设计”(上面在一章中引用),也许解决一般问题超出了我目前的范围。
2赞 Eric Postpischil 7/18/2023
@zgc:.0625 是 2 的最大幂,不大于示例中的数字 .1、.2 和 .3。