移动测试开发 揭开浮点数的谜团:陷阱、破解之道与测试要点
引言
在计算机中,浮点数和定点数是常用的数值表示方法,用于处理和表示实数(包括小数)的数值。它们在计算机中具有不同的特点和应用场景。浮点数广泛应用于科学计算、金融计算等需要大范围和高精度的任务中。然而,浮点数在表示和计算过程中存在精度等问题。在接下来的介绍中,我们将探讨浮点数的内部表示、常见问题以及处理这些问题常用的解决方案。此外,本文还总结了一些通用的浮点数测试用例,希望可以帮助大家更好地理解和验证浮点数的精度特性。希望通过阅读本文,大家能对浮点数有更清晰的认识和应用。
一、浮点数的表示
浮点数在计算机内部表示中,小数点的位置是可变的,也就是说,小数点可以"浮动"在有效数字中的任意位置,因此得名。那么,在计算机表示中,它的小数点数如何 “浮动” 的呢,一起来看看浮点数是如何表示的。
最常用的浮点数表示方法是 IEEE754,它是一种计算机浮点数算术标准,定义了浮点数的表示方法和浮点数运算规则。IEEE754 标准规定了两种浮点数格式:单精度浮点数(32 位)和双精度浮点数(64 位),分别用于表示单精度和双精度实数。
十进制转二进制
首先,我们要知道常用的十进制,在二进制的计算机世界中是怎样的。
十进制的正数部分和小数部分转换为二进制的方法是不一样的,十进制整数转二进制使用的是除 2 取余数法,十进制小数转二进制用的是乘 2 取整法。
我们举例说明一下,以十进制数 8.625 为例:
◆整数部分:将整数部分 8 转化为二进制,可以使用除 2 取余数法:
8 除以 2,商为 4,余数为 0,
将商 4 再次除以 2,商为 2,余数为 0
将商 2 再次除以 2,商为 1,余数为 0
将商 1 再次除以 2,商为 0,余数为 1。
将以上得到的余数按倒序连接起来,得到整数部分的二进制表示为:1000
◆小数部分:将小数部分 0.625 转化为二进制,可以使用乘 2 取整法:
0.625 * 2 = 1.25,整数部分为 1
剩余小数部分为 0.25。0.25 * 2 = 0.5,整数部分为 0
剩余小数部分为 0.5。0.5 * 2 = 1.0,整数部分为 1
剩余小数部分为 0。
将以上得到的整数部分按顺序连接起来,得到小数部分的二进制表示为:101
综上,8.625 的二进制表示为:1000.101
单精度浮点数
在 IEEE 754 中单精度浮点数(32 位)被拆成三个部分,分别是 sign、exponent 跟 fraction,加起来总共是 32 个 bit。下面以浮点数 0.15625 为例,看一下单精度浮点数的表示。
首先,需要将 0.15625 转化为二进制表示为 0.00101,其规格化(类似于十进制科学计数法)表示为 1.01 x 2-3。
在分别看一下单精度浮点数三个部分的内容:
sign:即符号位,最左侧的 1 bit 代表正负号,正数的话 sign 就为 0,反之则是 1
exponent:即阶码,中间的 8 bit 代表规格化后的次方数,采用的是 超 127 格式,也就是-3 还要加上 127 = 124,转化为二进制为 1111100;整数前面补零不会影响整数的值,则不足 8 位时在前面补零,即为 01111100。
fraction:即尾数,最右侧的 23 bit 放的是小数部分,以 1.01 来说就是去掉 1. 之后的 01。小数后面补零不影响小数的值,则尾数位不足 23 位在后面补零, 即为 01000000000000000000000。
十进制数 0.15625 表示成单精度浮点数就如下图所示:
双精度浮点数
单精度浮点数只用了 32 bit 来表示,为了让误差更小,提高精准度,IEEE 754 也定义了如何用 64 bit 来表示浮点数,即双精度浮点数。跟 32 bit 比起来尾数 部分大了超过两倍,从 23 bit 变成 52 bit,对比如下图所示:
双精度浮点数相对于单精度浮点数,指数位数的增加提供了更大的指数范围,可以表示更大和更小的数值,尾数位数的增加使得双精度浮点数可以提供更高的精度,能够表示更多的有效数字位数。
双精度浮点数的表示与单精度浮点数类似,就不在赘述了。
二、浮点数存在的问题
上文中介绍了浮点数的表示方法,因为其此种表示方法,浮点数在实际应用中也存在了一些问题,下面介绍下浮点数常见的问题。
精度问题
我们先看一个在实际业务场景中出现的浮点数问题举例:
业务逻辑上会把用户填写的数字进行 *100 的操作,如果填写 2.2,传入的参数就会变成 220,如下图所示,传入的参数并不是预期的 220,因为后端解析不了这种非整数,就会导致数据提交报错,但是如果填写的是 2.5,就不会有问题。
下面来探究一下原因:
首先看一下填写的十进制数 2.2 的二进制表示:10.0011001100110011001100110011001100110011001100110011...是一个无限循环的二进制小数。那么,它也就没办法表示为一个精确的浮点数,这个不精确的浮点数在进行乘法运算后,再转化为十进制,这个过程中,误差会累积变大,最终 2.2 乘以 100 得到的结果是 220.00000000000003,而不是精确的 220。
但是,十进制数 2.5 的二进制表示为 10.1,是一个正常的二进制小数,因此它可以表示为一个精确的浮点数,经过乘法运算和十进制转化,仍是精确的 250,就不会出现上述问题。
这个问题就是浮点数最常见的问题之一的精度问题,根本原因是浮点数表示的精度有限,无法精确表示所有实数,某些十进制数可能无法准确表示为有限位数的浮点数。
舍入误差
前面的例子中有提到 2.2 的二进制表示是无限循环的,由于浮点数的尾数部分有限,转换为浮点数时就会产生舍入误差,这个也是引起上面问题的原因之一。
比较问题
由于舍入误差,浮点数的比较操作可能会产生意外的结果,在某些情况下,两个看似相等的浮点数进行比较可能得到不相等的结果。比如下面这个实际项目中的例子:
页面展示的数据计算的可开票金额=51,496.64,填写该金额后,校验不通过,系统提示 “不能大于可开票金额",无法提交开票申请。
接口返回的数据如下图所示,并不是计算得出的预期结果 51,496.64,因此导致前端填写的数据与此数值比较时,无法验证通过。
溢出问题
我们上文中介绍浮点数表示说过,浮点数是有表示范围的,浮点数溢出就是指在浮点数计算中,结果超过了浮点数类型所能表示的范围。
不可结合性
浮点数的加法和乘法不满足结合律,因为舍入误差可能会导致不同的计算顺序得到不同的结果。如:a、b 和 c 是浮点数,(a + b) + c 和 a + (b + c) 的结果可能不相等。
解决方案
介绍了浮点数的存在的一些问题后,我们再来看看在实际项目中,解决这些问题的一些常用的方案。
使用高精度数据类型
对于需要更高精度的计算,可以使用高精度数据类型,比如现在普遍使用的 decimal,他是一种用于精确表示和计算十进制数的数据类型。
相较于其他浮点数类型(如 float 和 double),decimal 具有以下特点:
●高精度:decimal 使用固定的位数来表示数值,通常以小数点后的位数来衡量。这使得 decimal 能够提供更高的精确性和准确性。
●四舍五入:decimal 使用舍入规则,确保计算结果在保留有效位数的同时进行四舍五入,从而减少舍入误差。
●不受二进制表示误差影响:由于 decimal 使用十进制表示,而不是二进制,因此不会受到二进制浮点数表示误差的影响,提供更准确的结果。
●更广的数值范围:相较于其他浮点数类型,decimal 可以表示更广范围的数值,可以处理较大或较小的数值,同时保持精确性。
在许多编程语言中,例如 C#、Java 和 Python,都提供了 decimal 数据类型或相关的库或模块,用于处理需要高精度计算的场景。
尽管 decimal 数据类型在处理精确计算方面具有优势,但也存在一些缺点:内存占用较大、运算速度较慢、不适用于所有场景、不同编程语言和平台的兼容性问题等,在选择是否使用 decimal 数据类型时,需要权衡精度要求、计算性能和内存消耗,并结合具体的应用场景进行使用。
转化为整型运算
将浮点数乘以一个较大的倍数,然后转换为整型进行运算。在得到最终结果后,再除以相应的倍数来恢复精度。这样可以在整型范围内进行计算,并减少精度损失。
使用高精度数值库
某些编程语言提供了高精度数值库,如 Python 的 decimal 模块或 Java 的 BigDecimal 类。这些库可以提供更高的精度和控制,用于处理需要精确计算的场景。
通用测试点
总结了一下常用的测试点如下图所示,用于验证浮点数的不同场景下的处理逻辑,包括精度、边界情况和特殊值处理。
通过针对这些测试内容进行测试,希望可以帮助大家发现浮点数在不同情况下可能出现的问题,如精度损失、溢出、不可结合性等。有助于优化浮点数的处理逻辑,改进算法的准确性和效率,并确保浮点数在实际应用中得到正确处理。