《Unity3D高级编程之进阶主程》第一章,C#要点技术(三) 浮点数的精度问题

直接开门见山了。

浮点数的精度问题

平常极少使用double类型,因为浮点数计算我们也并没有使用到特别的科学计算部分,所以float基本都够用,而且double也同样有精度问题,无论怎么样都是无法避免精度问题在项目里的危害的。

===

float 与 double 比较来看,占用的位数不同会导致精度不同,但也会引起计算效率不同,内存消耗也不一样,虽然在现代计算机中来看,对double的计算量和占用的内存量已经不太在意了,在单片机中会有所限制,手机设备中是肯定没有限制的,不过无论怎么样它都是无法避免精度问题。

实际工作中想试图通过使用double替换float来解决精度问题的人数众多,最后基本都以失败告终。

要认清精度这个问题的根源,我们先来看下浮点数在内存中到底是如何存储的。

计算机只认识 0 和 1,所以数值都是以二进制的方式储存在内存中的。根据 IEEE 754 标准,任意一个二进制浮点数 V 均可表示为:V = (-1 ^ s) * M * (2 ^ e)。

以 198903.19(10) 为例,

先转成二进制的数值为:110000100011110111.0011000010100011(2)(截取 16 位小数),

采用科学记数法等于 1.100001000111101110011000010100011 * (2 ^ 17)(整数位是 1),

即 198903.19(10) = (-1 ^ 0) * 1.100001000111101110011000010100011 * (2 ^ 17)。

整数部分可采用 "除2取余法",小数部分可采用 "乘2取整法"。

从结果可以看出,小数部分 0.19 转为二进制后,小数位数超过 16 位(已经手算到小数点后 32 位都还没算完,其实这个位数是无穷尽的)。

由于无法得到完全精准正确的数值,所以浮点数在程序编程中就会遇到诸多的问题。

浮点数的精度问题,可不只是小数点的精度问题哦,当浮点数变大时,在整数部分也同样会有相同的问题,因为浮点数是一个X * Y的形式得到的数字,当数字放大时自然精度就会变小,无论是整数位还是小数位。

精度问题看起来好像很难碰到,但实际开发中碰到了很头疼。

那么到底哪些地方会碰到?

1.数值比较不相等

比如某个变量,需要从0开始加,每次加0.01,加到刚好0.23时做某事,到0.34时做另外一件事,到0.56时再做另一件。

这种精确定位的问题,就会遇到麻烦。因为浮点数在加减乘数时无法完全准确定位到某个值,就会在出现,要么比0.23小,要么比0.23大,永远不会刚刚与0.23相等的时候。

因此比较时,需要有一个微小的浮动区间。比如 ABS(X-Y) < 0.0001 时认为 X 和 Y 是相等的。

2.数值计算不确定

比如 x = 1f,y = 2f,z = 1 * 2f,如果 x / y <= 0.5f 时做某事,那么理论上说 x / z 也能通过这个if,但实际上未必能通过。

由于浮点数无法得到精确的数值而是一个浮动的数值。z 的计算结果有可能是0.4999999999991,当x / z 时,结果有可能得到大于0.5。

这就很头疼了啊。在实际编码中,如果在外圈的if判断成立,而内圈的if判断却有可能不成立,整个程序就出现无法估计得问题,因为看起来同样的数值,可能得到的结果却不一样。

这种情况下怎么办?

第一种解决方法是,只计算一次,认定这个值为准确值,只用这个变量结果做判断。

意识是说,用一次计算的结果,当做唯一确定性结果,而不再使用多次计算得到的结果。排除了多次结果不同导致的问题。

由于多次看似相等的计算其实得到的结果有可能不同,使得问题变得更复杂,比如上面所说的,1f / 2f 的结果,却用 1f / (1f * 2f) 来表示导致问题变得不可控。不如只使用一次计算结果,不再进行多次计算,认定当前结果的数值为准确数值,用这个浮点数值当做判断的标准。

我常用这种方法,因为其他方法涉及的代码量太多,情况也分不同种,限制太多,而项目需要快速推进,当前遗留的不是很重要的问题,完全可以推延到整体架构完成后,一步步细化,在前期的解决方案中,在考虑完整体架构的安全,负载能力,扩展能力,性能可优化性后,完全可以采用一些相对比较简单的解决方案,以快速达到前期的目标。

第二种解决办法是,改用int或long型来替代浮点数。

浮点数和整数的计算方式都是一样的,只是小数点部分不同而已,那么完全可以把浮点数乘以10的幂次,把自己需要的精度提上来用整数表示。

比如保留3位精度,所有浮点数都乘以1万来存储(因为第四位不是很准确了),1.5变成了15000的整数,9.9变成了99000整数存储。

这样整数 15000 乘以 99000 得到的结果,与,整数30000 除以 2 再乘以 99000 得到的结果是完完全全相等的。

再复杂点 原来 2.5 / 3.1 * 5.1 与 0.8064 * 5.1,两者约等于为 4.1126,用整数替代,2500 / 31 * 51 与 80 * 51,等于 4080,虽然精度出现问题,但前两者结果不一致,而后两者结果完全相同。

用整数做计算精度问题,我们可以再扩大数值的幂次么,来看看,如果是 250000 / 31 * 51 就等于 411290,是不是精度提高了啊。

但问题又来了,乘以10的幂次来提高精度时,当浮点数比较大时,就会超出了整数的最大上线2 ^ 32 - 1或者2 ^ 64 - 1。

第三种解决方法是,用字符串替代浮点数。

如果浮点数用途只有加减乘数,那么完全可以用字符串代替浮点数来计算结果。

我记得以前在大学里做ACM题目时,就有这种方式来检验程序员的逻辑能力和考虑问题的全面性的题目,题目很简单A * B 或 A - B 或 A + B 或 A / B 输出结果,精度要求在小数点后10位。

把中小学算术的计算方式,写入到程序里去,用整数计算当前位置的算算术问题,这样整数就完全不需要担心越界问题,而且能自由的控制精度。

缺点是,很消耗CPU,比如123456.78912345 * 456789.2345678,这种类型的计算使用字符串代替浮点数,用一次相当于计算好几万次的普通浮点数计算。但是如果程序中对精度要求很高,且计算的次数不大,我觉得还是可以把这种方式放在考虑范围内的。

第四种解决办法是,提高期望值。

如果 10000 / 2 有可能等于 4999.9999 而无法达到5000的目标值时,我们不妨在计算前多加个1,使得 10001 / 2,这样就大概率保证超出 5000 的结果目标了。

我们怎么想的?为什么能这么做?因为我们做了假设。

我们脑袋中假设结果是模糊的,这个结果范围就是有可能是5000.0001,也有可能是5000.02,也有可能是4999.998,也有可能是4999.98。

正常情况下,由计算机的寄存器和CPU决定到底会得到什么结果。但我们不想,我们的期望是,宁愿高一点点,跨过这道门槛,也不要少一点点,被门槛拦在外面。

如果浮点数的精度不能给我们安全感,那么我们就自己给自己安全感。提高自己的数值,或者说,降低门槛,差不多了就差一点点了,就当做是跨过去了。

于是就有了 (X + 1) / Y 或者 (X + 0.001) * Y 的写法来度过'精度危机'。

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第一章,C#要点技术(三) 浮点数的精度问题

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号