又见浮点数精度问题

今天看到一篇文章:http://younglab.blog.51cto.com/416652/241886,大概是说在使用Javascript进行下面的浮点数计算时出现了问题:

obj.style.opacity =  (parseInt(obj.style.opacity *100) + 1)/100;

obj.style.opacity是一个浮点数,范围从0~1,初始值为0。这句代码每隔一小段时间执行一次,从而让目标由透明慢慢变为不透明(淡入效果)。

问题是,起初obj.style.opacity还能够按照预期的每次以0.01逐步增加,但增加到0.29时就一直保持不变了。

作者只是记录了这个问题,没有写出为什么。读完这篇博客后我的第一感觉是:

这又是一个由于浮点数精度所引发的问题。

下面让我们来写一个小程序重现一下这个问题:

double opacity = 0;
for (int i = 0; i < 100; i++) {
     opacity = ((int) (opacity * 100 + 1)) / 100.0;

     System.out.println("opacity=" + opacity);
}

程序是用Java写的,共执行100次循环,采用了与那篇文章中相同的计算方法。正常情况下opacity会由0逐步增大到1。

程序输出如下:

opacity=0.01
opacity=0.02
opacity=0.03
opacity=0.04
opacity=0.05
opacity=0.06
(中间省略……)
opacity=0.27
opacity=0.28
opacity=0.29
opacity=0.29
opacity=0.29
……后面一直为0.29

可以发现,当opacity达到0.29后便不再增加了。由于Java和JS使用的是相同的浮点数格式,所以采用Java和JS结果都是相同的。

这里有一个细节需要注意:在这段程序中,除数必须写成100.0。这是由于在Java中有整数除法和浮点数除法两种不同的运算,如果写成100,那么被除数和除数将都是整数,Java就会按照整数除法来计算,就会导致每次计算的结果都是0(因为每次计算的结果都小于1,因此取整后就变为了0)。JS里没有这个问题,因为JS没有整数除法,所有除法都会当成浮点数除法来对待。

深入分析

现在我把上面那个程序做一点修改:

double opacity = 0;
for (int i = 0; i < 100; i++) {
     opacity = ((int) (opacity * 100 + 1)) / 100.0;

     System.out.println("opacity=" + new BigDecimal(opacity));
     System.out.println("opacity*100=" + new BigDecimal(opacity * 100));
     System.out.println("----------------------------");
}

因为Java在将浮点数转换为字符串时会做一些处理,让结果看起来更“美观”一些,但这样会让我们无法看清楚程序运行的真实情况。

在这个程序中我借助BigDecimal来显示浮点数在内存中的真正的样子。BigDecimal有一个以double数字为参数的构造方法,该方法会完整拷贝此double参数在内存中的位模式,它的toString( )方法也会严格按照实际的值进行转换,而不会为了“美观”而做任何处理。因此我们可以利用这种方法来看清一个double的“真面目”。

程序输出如下:

opacity=0.01000000000000000020816681711721685132943093776702880859375

opacity*100=1

----------------------------

opacity=0.0200000000000000004163336342344337026588618755340576171875

opacity*100=2

----------------------------

opacity=0.0299999999999999988897769753748434595763683319091796875

opacity*100=3

(中间省略……)

opacity=0.270000000000000017763568394002504646778106689453125

opacity*100=27

----------------------------

opacity=0.2800000000000000266453525910037569701671600341796875

opacity*100=28.000000000000003552713678800500929355621337890625

----------------------------

opacity=0.289999999999999980015985556747182272374629974365234375

opacity*100=28.999999999999996447286321199499070644378662109375

……后面一直重复相同的内容

可以发现,当opacity的值为0.29时,实际上在内存中的准确值是0.2899999……,所以乘以100变成28.99999……,这比29要稍微小那么一点点。但就是少了这一点点,当强制转换为整数后的结果却是28而不是期望的29。而这正是导致这个问题的原因所在。

从这个程序的运行结果中我们还可以观察到以下几个现象:

1. 每个中间结果例如0.01、0.02……等等,都无法用double类型精确表示

2. 即使本身无法精确表示,但在0.28之前,opacity*100的结果却都是精确的

3. 在无法精确表示的数中,有些比真实值略大,而有些却比真实值略小。如果是前者,当截断小数位转成整型时得到的结果是“正确”的;但如果是后者则会得到错误的结果。例如0.28*100转成整型为28,而0.29*100转成整型不是29而是28。

如何改正

经过前面的分析,现在我们已经弄明白了问题产生的原因,那么该如何修正它呢?

之前的代码之所以无法正确运行,其根本原因在于一个double类型的数字强制转换为整型时会发生截断,这会导致小数部分全部丢失,然而计算的中间结果中有一些要比期望的整数值略小,截断小数位以后得到的是比期望值小1的值。

因此我们可以从以下两个方面着手修正此问题:一是从代码中去除强制转换操作;或者,保证截断之前的中间结果一定是比期望值略大的。

方法1. 去除强制转换

程序的目的是让opacity的值每次增加0.01,那么就只需要每次加上0.01就好了,完全不需要绕圈子。如下:

double opacity = 0;
while (opacity < 1) {
     opacity += 0.01;
     System.out.println("opacity=" + opacity);
}

这个程序简单、直接,而且没有任何问题。我个人推荐这个方法。该程序输出如下:

opacity=0.01
opacity=0.02
opacity=0.03
opacity=0.04
opacity=0.05
opacity=0.060000000000000005

(中间省略……)

opacity=0.9800000000000006
opacity=0.9900000000000007
opacity=1.0000000000000007

方法2. 保证截断之前的中间结果略大于期望值

既然原程序的问题发生在截断时,那么只要保证截断发生之前,中间结果的值略大于期望值,就能保证程序的正确性。例如如果要让截断后的结果为29,只要保证截断前的值在[29, 30)这个范围内即可。

如何做到这一点呢?

由于我们可以肯定在这个问题中,opacity*100的结果是非常接近我们所期望的整数的,只是由于double类型的精度限制而比期望的整数略大或略小而已,其误差一定非常非常小。

所以我们可以修改这句代码:

        opacity = ((int) (opacity * 100 + 1)) / 100.0;

不是给opacity * 100加上1,而是加一个更大一些的数,例如1.5,变为:

        opacity = ((int) (opacity * 100 + 1.5)) / 100.0;

如果我们期望的值是29,那么修改后的中间结果一定是在29.5附近,这样就能保证截断后的值一定是29了。程序如下:

double opacity = 0;
for (int i = 0; i < 100; i++) {
     opacity = ((int) (opacity * 100 + 1.5)) / 100.0;

     System.out.println("opacity=" + opacity);
}

输出为:

opacity=0.01
opacity=0.02
opacity=0.03
opacity=0.04
opacity=0.05
opacity=0.06

(中间省略……)

opacity=0.96
opacity=0.97
opacity=0.98
opacity=0.99
opacity=1.0

可以看到结果是正确的。

总结

只要稍有经验的程序员都知道浮点数不能直接进行相等比较,但是像这篇文章中所碰到的问题可能并不那么常见,因此有时不容易意识到发生了问题。

每个程序员都应该知道计算机中是采用近似值来保存浮点数的,当进行浮点数相关的计算时,需要时刻提防由于精度问题所导致的误差,并注意避免那些会影响到结果正确性的误差(所谓正确性,就是误差超出了所允许的最大范围)。

附:

下面这个网页列举了历史上的一些由于计算问题引起的软件灾难,其中一例是1996年欧洲航天局的Ariane 5火箭发射失败事件,该火箭发射后仅40秒即发生爆炸,导致发射基地的2名法国士兵死亡,并导致历时近10年、耗资达70亿美元的航天计划严重受挫。事后调查报告显示问题的原因出在火箭的惯性参考系的软件系统中,其中有一个地方是将水平方位的64位浮点数转换为一个16位的整数,当浮点数的值超过32767时,转换就会失败(即转换的结果是错误的),从而导致了悲剧的发生。

http://ta.twi.tudelft.nl/users/vuik/wi211/disasters.html

时间: 2024-08-01 10:46:05

又见浮点数精度问题的相关文章

一个由于浮点数精度导致的错误

今天看到一篇文章:http://younglab.blog.51cto.com/416652/241886,大概是说在使用Javascript进行下面的浮点数计算时出现了问题: obj.style.opacity = (parseInt(obj.style.opacity *100) + 1)/100; obj.style.opacity是一个浮点数,范围从0~1,初始值为0.这句代码每隔一小段时间执行一次,从而让目标由透明慢慢变为不透明(淡入效果).起初,obj.style.opacity能够

mingw fbx sdk /浮点数精度

接下来要做一个linux下的程序了. 下载linux version     fbx sdk tar zxvf ...gz 按照安装说明 提升权限并没什么用 还是,cannot execute binary file 感觉是版本的问题,也就是说我要用f extension bx sdk这个版本 是dll的 vs跑完用mingw windows + vs2013用的肯定是 febx sdk windows version mingw 下面,据说那只是gcc而不意味着 linux所以...也许还是要

.Net Entity Framework Core 用 HasColumnType 配置浮点数精度

一.前言 前段时间用.Net Entity Framework core搭建框架,需要配置浮点数的精度,发现.Net Entity Framework core 并没有HasPrecision方法.在网上查找资料也比较少,最后通过官方文档说明,尝试使用HasColumnType配置浮点数精度成功. 二.HasColumnType官方文档说明 文档连接: https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.entityframeworkcor

【解码】浮点数精度问题 | 为什么(int)(32.3 x 100) = 3229?

零 | 序 前几天在找一个代码问题时,苦思不得其解,简直要怀疑人生.查看各种参数,输入输出,都符合条件,最后各种排除法之后,定位到一段简单的代码,简化后大致如下: #include<stdio.h> int main() { double a = 32.3; int b = 100; int c = (int)(a*b); printf("c = %d",c); //c = 3229 return 0; } 原代码中本来预想c应该会等于3230,可是最后的结果却是3229!

Java 浮点数精度丢失

Java 浮点数精度丢失 问题引入 昨天帮室友写一个模拟发红包抢红包的程序时,对金额统一使用的 double 来建模,结果发现在实际运行时程序的结果在数值上总是有细微的误差,程序运行的截图: 输入依次为:红包个数,抢红包的人数,选择固定金额红包还是随机金额红包,每个红包的金额(此例只有一个红包). 注意到程序最后的结果是有问题的,我们只有一个金额为 10 的红包,一个人去抢,所以正确结果应该为这个人抢到了 10 RMB. 为了使问题更加明显,我们测试一个更加简单的例子: public class

用decimal模块增加python的浮点数精度

浮点数python默认是17位精度,也就是小数点后16位(16位以后的全部四舍五入了),虽然有16位,但是这个精度越往后越不准. 如果有特殊需求,需要更多的精度,可以用decimal模块,通过更改其里面getcontext()函数里面的prec参数,来决定你想要的浮点数精度. from decimal import Decimal from decimal import getcontext work_context = getcontext() work_context.prec = 100

浮点数精度问题

浮点数精度问题: public static void main(String[] args) { double d = 11.4; int i = 13; double a = d*i; System.out.println("反例:"+a); BigDecimal num12 = new BigDecimal(d); BigDecimal num22 = new BigDecimal(i); BigDecimal result32 = num12.multiply(num22);

js浮点数精度丢失问题及如何解决js中浮点数计算不精准

js中进行数字计算时候,会出现精度误差的问题.先来看一个实例: console.log(0.1+0.2===0.3);//false console.log(0.1+0.1===0.2);//true 上面第一个的输出会超出我们的常识,正常应该为true,这里为什么会是false呢,直接运行会发现0.1+0.2在js中计算的结果是: console.log(0.1+0.2);//输出0.30000000000000004 这对于浮点数的四则运算(加减乘除),几乎所有的编程语言都会出现上面类似的精

Java之道系列:BigDecimal如何解决浮点数精度问题

如题,今天我们来看下java.math.BigDecimal是如何解决浮点数的精度问题的,在那之前当然得先了解下浮点数精度问题是什么问题了.下面我们先从IEEE 754说起. IEEE 754 IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用.这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number)),一些特殊数值(无穷(Inf)与非数值(NaN)),以及这些数值的"浮点数运算符&qu