行则将至

人生在勤,不索何获

0%

浮点数精度问题及解决方案

原文地址 fivecakes.com

浮点数精度问题

浮点数精度问题示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main()
{
float a = 0.1;

printf("%f\n",a);
printf("%.10f\n",a);
printf("%.15f\n\n",a);

float b = 100000000;
float c = 5;
float d = b + c;
printf("%f\n\n", d);

return 0;
}
输出为
1
2
3
4
5
0.100000 		    //正确输出是因为四舍五入了
0.1000000015 //将精确度提高就会发现误差
0.100000001490116 //再次提高精度

100000008.000000 //加法居然算错了?傻了吧?

浮点数精度误差原因

1. 第一种造成精度误差的原因是大多数小数转化为二进制为无限小数,而计算机内存有限,只能保存一部分,

例如:
0.1 转换为二进制是 0.0001100110011001100110011001100...
float 只能保留一部分,所以一定会有误差,好在大多数场景用不到这么高的精度,不过要注意多个小误差累积成大误差。

2. 第二种造成精度误差的原因是因为较大的浮点数与较小的浮点数相加。

例子中 100000000 + 5 居然等于 100000008,这是因为浮点数是用科学计数法来存储数据,即:
\(100000000 = 1.011111010111100001 \times 2^{26}\) \(5 = 1.01 \times 2^{2}\)
两个数相加的时候先要对阶,就是把指数部分统一,按照小阶数化为大阶数的原则:
\(5 = 0.00000000000000000000000101 \times 2^{26}\)
按照 IEEE 754 标准,浮点数 float 为 32 位,1 个符号位,8 个指数位,23 个小数位。 而 0.00000000000000000000000101(小数点后有 26 位)不能用 23 位保存下来,所以舍入成 0.00000000000000000000001(小数点后有 23 位),就是这里的舍入造成的误差。
对阶后,将尾数相加得到结果:
(0.00000000000000000000001+1.011111010111100001)^{26} = 100000008

浮点数精度误差问题解决方法

浮点数在工程控制,游戏引擎等众多领域有着广泛的应用。但是因为其有误差,所以不适合在金融领域使用。

某水果 3.6 元一斤,某人买了 1.2 斤,总计 4.32 元。如果用 float 存储价格的话,一单的误差比较小看不出差别。但是如果统计一年的营业额,累加很多 float 就会出现较大的误差。

放大成int再还原法

货币通常只精确到小数点后两位,所以我们可以将其乘 100,用整数存储 。上面那个例子可以将 3.6 存成 int 型 360,1.2 用 float 存储,4.32 存成 int 型 432(其实 432 由浮点数计算而来也是有误差的,但是一次计算误差非常小,四舍五入就可以得到正确的值)。累加一年营业额时累加的是 int,所以不会有误差。

保存分子分母法

更特殊的情况,某水果 3.6 元一斤,某人买了 1.2 斤,75 折,含税 0.02,快递费 1.5,总价为 3.6x1.2x0.75x0.98+1.5,多次使用浮点数参与运算,有可能造成累积误差。这种情况可以可以分子和分母分别保存 。计算时只计算分子,计算的结果也分子分母分别保存,只有显示时才除一下分母,而一次计算的误差非常小,四舍五入就可以得到正确的值。不过用这种方法要小心分子和分母溢出,或者写到一半发现这里要开个平方根或者求个三角函数,然后发现结果根本不是有理数了,进而开始怀疑人生。

Backlinks: