来看0.1+0.2是否等于0.3
fn main() {
assert!(0.1 + 0.2 == 0.3);
}
// Rust中的assert!宏用于在代码中检查条件是否满足,
// 如果条件为假,程序将触发一个panic并终止执行。
运行结果是
assertion failed: 0.1+0.2=0.3
显然,浮点运算0.1+0.2是不等于0.3的。在IEEE754标准下,这三个数都不能精确表示。浮点数是编程语言中常见的数据类型,计算机中是如何表示浮点数的?
参照十进制科学计数法,二进制浮点数也可表示为
其中 叫做阶码, 叫做尾数。这样,表示一个浮点数需要分三个部分--符号、阶码和尾数。
▲图1
IEEE754标准中浮点数的表示格式如图1所示,32位的浮点数0-22位表示尾数,23-30位表示阶码,最高位表示符号,即0表示正数,1表示负数。
阶码采用移码表示,即在真值阶码的基础上加上一个偏移值(32位浮点数加127),所以实际的范围是 , 即 。这样可保证所有的阶码都是无符号整数,省去了表示符号的麻烦,且最小真值的移码为全0,最大真值的移码为全1,符合实际生活中的习惯。另外,移码保持了真值原有的大小顺序,采用移码表示还可以直观的比较大小,如图2所示,右起第一列是移码表示,具有和真值一样的大小顺序。
▲图2
尾数是定点小数,用原码表示。
IEEE754标准中浮点数分为规格化(Normalized)和非规格化(Denormalized)两种。规格化浮点数的尾数的范围是 ,即 ,非规格化浮点数的尾数的范围是 ,即 。这样表示尾数时还可以把小数点左边的那一位省去。
当尾数的23位bit位全为1,且阶码最大127时,通过变更符号位,可得到32位浮点数的表示范围约为 。注意规格数的尾数始终大于1,而阶码始终大于0, 即便 非常小, 但还是大于0。当尾数的23位bit位全为0,且阶码最小-126时,规格化浮点数能表示的最小正数就是 ,最大负数 。也就是说,使用规格数时, 我们除了无法表示0, 也无法表示 之间靠近0的极小数,这种现象叫做下溢(underflow),如图3所示
▲图3
这就是规格数的局限性, 这个局限性将由非规格数解决。
非规格数尾数的取值范围是 , 阶码固定为-126. 让尾数取 之间的数, 并通过变更符号位, 我们可以表示出 之间靠近0的极小数。特别地,当尾数的23位bit位全为0时,并通过变更符号位, 我们可以表示出+0和-0, IEEE754规范中也确实存在着这两种表示0的方式。
加入非规格化数后,IEEE754单精度浮点数的范围有如下变化
▲图4
非规格数的取值范围,正好可以卡在规格数取值范围的中间, 现在我们得到了一个完整的取值范围:
▲图5
如图5所示,IEEE754标准中还有两类特殊数: Infinity和NaN。Infinity的表示很简单, 其符号位可正可负,8位阶码位全为1,且23位尾数位全部为0。
对于NaN,其符号位可正可负,8位阶码位全为1,23位尾数位不全为0。NaN表示“不是一个数”,例如负数的平方根就是NaN, NaN会影响其它数值,几平所有与NaN交互的操作都返回NaN,两个NaN值永远不相等
fn main(){
let x =(-42.0_f32).sqrt();
assert_eq!(x,x)
}
运行结果是
assertion `left == right` failed
left: NaN
right: NaN
回到开头的问题,十进制的0.1、0.2和于0.3怎么表示成IEEE754的浮点数?以下的python脚本可将十进制小数转为二进制
def dec2bin_ex(target):
#将target分离成整数和小数部分
i = int(target) #整数部分
f = target - i # 小数部分
#将整数部分转换为二进制数
a = [] #剩余部分的列表
while i!= 0:
a.append( i%2 ) # 余数
i = i // 2 # 商
#把元素按相反的顺序排列
a.reverse( )
#将小数部分转换为二进制数
b = [] #带有整数部分的列表
n = 0 #重复的次数,大于10次停止
#乘以2直到小数部分为0
while (f !=0):
temp = f*2 #小数部分x2
b.append( int(temp) ) #整数部分
f = temp - int( temp ) #小数部分
n += 1
if( n >= 10):
break
#值转换为二进制, 结果格式为([整数部分],[小数部分]
return a, b
例如,将10.625转为二进制
>>> dec2bin_ex(10.625)
([1,0,1,0], [1,0,1])
十进制数10.625转换为二进制数,即1010.101。那么0.1呢?
>>> dec2bin_ex(10.625)
([], [0,0,0,1,1,0,0,1,1,0])
如果保留小数点后十位,就是 。现在将其用IEEE754标准表示。
首先,移动小数点的位置,使二进制数的整数部分变成1。例如,将 的小数点向左移动3位,得到 。如果再乘以移动位数的权重,则为 ,这样就可以恢复到原来的值(参考十进制)。以同样的方式,我们用指数来表示 。这一次,我们将小数点向右移动4位,所以得到 。
同样,对于 和 。无论你在小数点之后保留多少位,你都不能用精准的二进制数来代替它们。并且容器的大小是有限的,保留有限位数将产生非常小的误差。当然,即使是最先进的计算机也不能消除这种误差。重要的是,我们要明白,当我们与计算机互动时,会有误差。
一般来说,测试数学运算是否在其真实数学结果的可接受范围内更安全。这个边界通常被称为 。Rust提供了一些可容忍的误差值,比如f32::EPSILON和f64::EPSILON
fn main(){
let result:f32 = 0.1 + 0.2;
let desired:f32 = 0.3;
let abs_diff = (desired - result).abs();
assert!(abs_diff < f32::EPSILON)
}