我正在学习单精度,并希望了解错误传播。根据{{3}},添加是危险的操作。
所以我编写了一个小型C程序来测试错误加起来的速度。我不完全确定这是否是一种有效的测试方式。如果是,我不确定如何解释结果,见下文。
#include <stdio.h>
#include <math.h>
#define TYPE float
#define NUM_IT 168600
void increment (TYPE base, const TYPE increment, const unsigned long num_iters) {
TYPE err;
unsigned long i;
const TYPE ref = base + increment * num_iters;
for (i=0; i < num_iters; i++ ) {
base += increment;
}
err = (base - ref)/ref;
printf("%lu\t%9f\t%9f\t%+1.9f\n", i, base, ref, err);
}
int
main()
{
int j;
printf("iters\tincVal\trefVal\trelErr\n");
for (j = 1; j < 20; j++ ) {
increment(1e-1, 1e-6, (unsigned long) (pow(2, (j-10))* NUM_IT));
}
return 0;
}
执行结果
gcc -pedantic -Wall -Wextra -Werror -lm errorPropagation.c && ./a.out | tee float.dat | column -t
是
iters incVal refVal relErr
329 0.100328 0.100329 -0.000005347
658 0.100657 0.100658 -0.000010585
1317 0.101315 0.101317 -0.000021105
2634 0.102630 0.102634 -0.000041596
5268 0.105259 0.105268 -0.000081182
10537 0.110520 0.110537 -0.000154624
21075 0.121041 0.121075 -0.000282393
42150 0.142082 0.142150 -0.000480946
84300 0.184163 0.184300 -0.000741986
168600 0.268600 0.268600 +0.000000222 <-- *
337200 0.439439 0.437200 +0.005120996
674400 0.781117 0.774400 +0.008673230
1348800 1.437150 1.448800 -0.008041115
2697600 2.723466 2.797600 -0.026499098
5395200 5.296098 5.495200 -0.036231972
10790400 10.441361 10.890400 -0.041232508
21580800 25.463778 21.680799 +0.174485177
43161600 32.000000 43.261597 -0.260313928 <-- **
86323200 32.000000 86.423195 -0.629729033
如果测试有效
0.1
表示为例如0.100000001
,不管累加的数量多少,这不应该总是累积到相同的偏差吗?168600
总结有什么特别之处(参见*
)?错误变得非常小。可能是巧合。incVal = 32.00
点击了哪一面墙(见**
,后两行)。我仍远低于unsigned long
限制。提前感谢您的努力。
答案 0 :(得分:2)
首先,要知道0.1
无法准确表示,以二进制形式定期重复数字,这一点非常重要。该值为0.0001100110011...
。比较1/3和1/7用十进制数字表示的方式。值得以0.25
为增量进行测试,这可以用0.01
完全表示。
我将用十进制来说明错误,这是我们人类习惯的错误。让我们使用十进制,并假设我们可以有 4 精度数字。这些都是这里发生的事情。
分部:让我们计算1/11:
1/11等于0.090909 ...,可能四舍五入为0.0 9091 。正如预期的那样,这是正确的4位有效数字(粗体)。
幅度差异:假设我们计算10 + 1/11。
当添加1/11到10时,我们必须进行更多舍入,因为10.09091是7位有效数字,而我们只有4位数。我们必须在点之后将1/11舍入到两位数,并且计算的总和 10.09 。这是一种低估。注意如何仅保留1/11的一个有效数字。如果将大量小值一起添加,则会限制最终结果的精度。
现在计算100 + 1/11。现在我们将1/11舍入到0.1并将总和表示为 100.1 。现在我们略微过高估计,而不是略微低估。
我的猜测是你的测试中符号变化的模式是系统轻微低估与高估的影响,具体取决于base
的大小。
1000 + 1/11怎么样?现在我们在该点之后不能有任何数字,因为我们在该点之前有4位有效数字。 1/11现在四舍五入为0,总和仍然 1000 。那是您正在看到的墙。
您在测试中看不到的另一件重要事情是:如果两个值的符号不同,会发生什么。计算1.234 - 1.243:这两个数字都有4位有效数字。结果是-0.009。现在结果只有一个正确的有效数字而不是四个。
这里回答类似的问题:How does floating point error propagate when doing mathematical operations in C++?。它有一些指向更多信息的链接。
答案 1 :(得分:1)
回答你的问题......
1 - IEEE浮动到甚至 mantissas。这是专门为了防止错误累积总是以某种方式偏置而完成的;如果它总是向下舍入或向上舍入,那么你的错误就会大得多。
2 - 168600本身并没有什么特别之处。我还没有把它弄平,但它完全有可能最终使二进制表示更清晰(即理性/非重复值)。查看二进制值,而不是十进制值,看看该理论是否成立。
3 - 限制因素可能是由于浮动尾数为23位长。一旦base
变为特定大小,increment
与计算base
的{{1}}相比非常小,然后将尾数舍入到23位完全消除了更改。也就是说,base + increment
和base
之间的差异是舍入错误。
答案 2 :(得分:1)
你正在击中的“墙”与增量值无关,如果它通过加法是恒定的,你从零开始。它必须与iters
。 2 ^ 23 = 8百万,你正在增加8600万。因此,一旦累加器比增量大2 ^ 23,就会撞墙。
尝试使用86323200迭代运行代码,但增量为1或0.0000152587890625(或任何2的幂)。它应该具有与32的增量相同的相对问题。