IEEE754浮点数表示法
作者:互联网
IEEE二进制浮点数算术标准(ANSI/IEEE Std 754-1985)是一套规定如何用二进制表示浮点数的标准。就像“补码规则”建立了二进制位和正负数的一一对应关系一样,IEEE754规则说明了一个从二进制状态到实数集的一一映射的规则(当然事实上状态有限而实数无限,叫做“单射”更为合适)。
IEEE754的初标准在1985年发布,也是现在广为流传的版本,被大多数语言所采用。事实上后来已经有了更新的标准了,不过两者间没有太大的区别。因此了解老标准就可以。
浮点数是如何存储的
标准提供了四种最常见的规范:
- 单精度(single)浮点(32bit)
- 双精度(double)浮点(64bit)
- 延伸单精度(extended single)浮点(43bit以上,很少用到)
- 延伸双精度(extended double)浮点(79bit以上)。
沿用C/C++习惯,可以用float
代指32位单精度浮点、double
代表64位双精度浮点。以下主要以较短的float
进行说明。
一个32位float
型数用科学计数法表示,由符号位1位(sign)、指数位8位(exponent)和小数位23位(fraction)组成,在图里从左到右排列。
一个64位double
型数由符号位1位、指数位11位和小数位52位组成,在图里从左到右排列。
- 符号位:1位,
0
表示正数,1
表示负数 - 指数位:8/11位表示指数。可以表示256/2048种状态。
然而指数是可正可负的。在标准里,我们没有选择用"补码规则"表示负数,而是选择直接向左平移(又叫阶码)。8位范围是\([0,255]\),我们将它向左平移一半(取127),就变成了\([-127,128]\),也就是说指数位减去127才是真实的指数(比如12(00001100)
代表-125
次方)。这里减去的数叫偏移量(biase),对单精度来说是127,对双精度来说是1023。 - 小数位:23/52位,表示底数。显然底数的长度决定了类型的精度,决定了到底能存几位有效数字,而指数位只是表示小数点的位置
二进制里的科学计数法
十进制和二进制的互化大家都很熟悉,但是一般仅限于整数,许多计算器软件在二进制下甚至不能输入小数点。
不过小数的转化其实也是一个道理:对于整数位来说,第\(i\)位的1代表\(2^i\),而小数点后的第\(i\)位1则代表\(2^{-i}\)。比如\(110.101_{bit}=4+2+\frac 12 +\frac 18=6.625\)
将十进制数化为二进制数就反过来弄:小数部分大于0.5,则第一位为1,小数部分"模0.5"后大于0.25,则第二位为1。。。比如\(0.875=0.111_{bit}\)
在十进制中,如果用科学计数法表示数,最规范的表示就是让底数的小数点之前仅有一位非零整数,便于用指数表示数量级。在二进制中,我们也这样干,并且可以得到更特殊的性质:二进制中的"非零整数"只能是1。也就是任意一个不是太接近0的数都可以表示为\(1.xxxx \times 2^{exp}\)的形式。因此在表示小数位时,我们将这个首位1省略,只保存小数部分,显然对于一个不是太接近0的数,这样的表示都是益于节省空间提高精度的。
写出一个数的浮点表示
-
实战演练:将\(78.625\)转化为浮点数形式。
\(78.625\)的二进制形式是\(1001110.101\),即\(1.001110101\times 2^6\),而指数位\(6+127=133=10000101_{bit}\),将底数的小数位后面补0到23位,得答案01000010100111010100000000000000 -
在C++中,浮点数是不能给二进制位赋值的。但是我们可以将32位整数赋值为对应的数,再用float指针来解析它,验证结果(后文也会写成16进制,节省空间)。
特殊的浮点位
IEEE754标准还提供了浮点数中一些特殊状态的表示。
非规约数 & 正零和负零
上述规则描述的是常规范围内的数如何表示,他们可以叫做规约数(normal number)。高位1的省略可以节省空间。这样最接近0的数(即0x00000000
)值为\(\pm 2^{-127}\)
但是如果一个数太小,他的第一位有效数字(当然指二进制)在127位以后呢?即使小数点右移127位,最高位仍然是0,不能表示更小的数了。
为了表示更小的数,在指数位全为0时,我们丢掉最高位为1的束缚,将最高位规定为0,将"全0指数位"规定为-126而不是本来的-127,用于表示绝对值小于\(2^{-126}\)的数。
比如00000000000101000000000000000000,其值为\(0.00101\times 2^{-126}=1.01*\times 2^{-129}\),表示出了更小的数。在这样的规则下,最接近0的数(即0x00000001
)值为\(\pm 2^{(-127-23)=-149}\),而全零位用来存储0。
这样的“全零位”,由于符号原因有两种(0x00000000
和0x80000000
),他们用于表示正零和负零。高级应用层面对于正零和负零的判定各不相同。在C++,正零和负零是相等的,并且都对应布尔值false
(尽管负零的符号位)。我们不关心,我们只需要知道IEEE支持两种零的表示,并且在运算过程一个理论答案为零的结果既可能被计算为正零,也可能被计算为负零。
逐渐溢出
规格数的最小值为\(0(00000001)0..0_{bit}=2^{-126}\),非规格数的最大值为\(0(0..0)1..1_{bit}=(1-2^{-23})2^{-126}\),基本可以看做\(2^{-126}\)的开区间,从非规格数过渡到规格数时,相当于指数-126不变,底数进位到隐藏的高位。从而实现了平稳的值域过渡,刚好覆盖了实数轴,这种特性叫做逐渐溢出(gradual overflow)
更有意思的是,当二进制码从0x00000000
不断递增时,他表示的浮点数值也是逐渐递增的。对于非规约数到规约数来说表现为"逐渐溢出";对于规约数来说,小数部分没有全满的情况显然;而每当小数位全为1时,再下一个数应该是"逢二进一"(小数位清零,指数位加一),就好像小数位像指数位进位了一样(比如0(0..01)11..11
对应浮点数的下一个数是0(0..10)00..00
,而0(0..01)11..11
对应整数的下一个数也是0(0..10)00..00
)!根据这个特性,我们也可以对浮点数进行基数排序(先划分正负,同号的数将后31位任意切割为多个关键字后分别排序)。
无穷
为了表示状态"无穷",同样只能从指数上动手脚。我们把指数全为1的状态"挖掉",用于表示无穷等状态,如果一个数指数位全为1,小数位全为0,那么这个数就表示无穷。
显然无穷有两种,\(0(1..1)0..0_{bit}\)对应正无穷0x7f800000
,\(1(1..1)0..0_{bit}\)对应负无穷0xff800000
。无穷支持一些数学意义上的运算:
- 同号无穷被认为相等,正无穷>所有规约数>负无穷
- 无穷与规约数进行四则运算仍是无穷
C++用1/0.0
或者1e1000
或者1e10000000
赋值就可以得到一个无穷,他们都是一样的无穷,本质上是表示"超过存储范围"。可以输出无穷,表示为inf
和-inf
。
非数值
实数范围里,有一些计算是没有结果,无法进行的。在标准里同样规定了一类数,用于保存这类结果,他们叫做非数值(not a number)。非数值与无穷一样使用全为1的指数位表示,为了区分开来,小数位全为0时表示无穷,其他所有情况表示非数值情况。
显然很多状态都可以表示非数值,但是他们不被加以区分,也不分+NaN或者-NaN,同时也不能参与运算。
-
C++中,NaN与任何数的算数比较将返回
false
。即使是自身之间(实际上NaN==NaN
、NaN<NaN
、NaN>NaN
均为假,只有NaN!=NaN
为真)。NaN自身转化为bool值后为true
-
任何NaN参加的运算,结果仍然是NaN
C++中用sqrt(-1)
、0.0/0.0
或者inf-inf
都将得到NaN,可以将其输出,表示为nan
。
浮点数的范围和精度
对于32位规约数来说,指数位包括\([-127,128]\),但是左右端点用来表示特殊数了,因此实际指数位\([-126,127]\)
首先是范围,这个很好计算。不妨只考虑正数,前面已经计算过最小的规约数为\(2^{-126}\),而最大的规约数应该是\(0(11111110)1..1_{bit}\approx 2\times 2^{127}=2^{128}\),因此极限范围就是\([2^{-126},2^{128})\),转化为十进制就约是\([1.175\times 10^{-38},3.403\times 10^{38}]\)。如果算上非规约数0x00000001
,下界可以达到\(2^{-149} \approx 1.401\times 10^{-45}\)。
而关于精度也不难计算,精度即底数有效数字的位数,底数有23位,那么可以表示\(2^{23}\approx 10^{6.92}\)种有效数字,即两个形如\(1.xxxxx\)的23位数大致可以和十进制下的7位小数一一对应,7位以后不同的数字只能对应到同一个二进制数上。
浮点数是离散不均匀储存的
对于整数来说,32位二进制码与\([0,2^{32})\)的数一一对应,是多少就是多少。\([0,2^{32})\)里的全体整数可以看作对应关系的"值域"(一张数表)。如果赋值int a=3.4
呢,值域里没有这个数!于是只能将它存为值域里最相邻的两个点之一(C++中浮点阶段为整数的规则是向0取整(做图时写错了应该是3),但是你也可以处理为向上取整,向下取整,或者四舍五入)。
而对于浮点数来说(仍以float
为例),32位二进制码最多只能对应\(2^{32}\)个数,但是实数是无穷无尽的!因此,按照上面规则,除去无穷和非数值,每个状态计算出一个实数组成值域,float
只能表示这些有限多的实数,对于不在"值域"内的数,只能选择将他存储为相邻的两个点之一(8388607.2在float
范围里,但float
的数表里没有这个数)。
显然,相邻两个数越近,误差越小,精度越高,小数部分越长,越能支持更大精度。如果只考虑同一个类型,float
的精度是多少呢?
我们从1开始计算,1的表示为\(1*2^0\),1的下一个数是\((1+2^{-23})*2^0\),再下一个数是\((1+2^{-22})*2^0\),由于实际指数为0,因此小数位每移动1,值就移动\(2^{-23}\approx 1.19\times 10^{-7}\)。
但是在2048附近呢?2048的表示为\(1*2^{11}\),下一个数\((1+2^{-23})*2^{11}\),再下一个数是\((1+2^{-22})*2^{11}\),误差增大到了\(2^{-12}\approx 2.4\times 10^{-4}\)。
规律已经很显然了,和整数完全不同,浮点数的间隔是变化的,离0越远,间隔越大,并且每通过一个\(2^i\),指数位就增大1,间隔增大一倍。用刚刚有效数字来理解,有效数字只有大约7位,随着整数部分越来越大,小数部分的位数会越来越短,在上图,间隔已经达到0.5,只能储存整数和"整数.5"。在数据达到\(2^{23}=8388608\)以后,间隔达到1,小数部分消失,小数都会舍入到整数。数据达到\(2^{24}=16777216\)以后.间隔变为2,已经不能精确存储整数了。
这也可以说明为什么float
的范围看起来如此夸张,因为这不算真的可用范围,只是表示无穷以下的最大值而已。这个数可以表示为\(2^{60}\),却不能表示\(2^{60}+1\),也不能表示\(2^{60}+1e9\),他的下一个数是\(2^{60}+2^{37}\),这是完全不可用的。
冷知识:《我的世界》中当水平坐标超过一千万时,图像扭曲,加载异常,默认情况不能出现的5格跳,以及最终出现的边境之地等都被认为是浮点误差太大引起的
进制影响真实的储存位数
为什么0.1+0.2=0.300000000004?,一位有效数字也不能精确储存了?这是因为0.1和0.2知识看上去的一位,实际上是无限小数。
我们知道任何\(\frac pq\)只有在分母的因子都能被进制整除才能写成有限小数。比如10的因子只有2和5。所以\(\frac p{2^n5^m}\)无论分母多大也能除得尽。但\(\frac 13\)一个简单的数却只能写成无限循环。在之前的例子里,二进制有限小数化为十进制当然有限(显然),但十进制有限在二进制下不一定有限,因为二进制无法把数五等分。
-
0.2,按照开篇的规则,应该对应\(0.0011\ 0011\ 0011..._{bit}\),在某一位后截断在存储,实际值不是0.2,是值域中最接近0.2的数。
-
0.99993896484375,尽管远大于7位,但它可以在二进制下完全表示\(1-2^{-14}=0.11111111111111_{bit}\),因此完全储存在了float中, 体现出了超乎寻常的精度。
所以之前提到的精度只是约值,而且其意义应该是相邻两数的间隔值,即"存储值和实际值相减后大约第7位才有明显误差"。绝不是说"7位以下的数字能精确存储,7位以上的就截断到7位"。
其他类型的浮点数
以上说的都是32位,其实对于64位来说也是一样的,更进一步来说,现在的标准还指定各种位数浮点数的存储标准,一般来讲,位数越长,小数位越多,有效数字越多;同时指数位也越多,最大范围更大。虽然范围不一样,但他们的标准是一样的。
-
半精度(Half)(16bit)
-
四精度(Quadruple)(128bit)
-
八精度(Half)(256bit)
-
延伸精度与上面的又不太一样。(就好像32位整数相乘时,要取到64位一样)延伸精度可以视为"精度运算的中间变量"。延伸双精度定为79位以上,便于执行比双精度更精确的计算。一些储存标准中为扩展精度提供了专门的最高位。按照维基百科最高位的存在使延伸精度可以表示更多"额外状态",比如运算中的精度损失。C++里
long double
可以实现延伸双精度,长度为80/96/128位。
标签:表示,..,浮点数,IEEE754,NaN,表示法,无穷,精度 来源: https://www.cnblogs.com/ofnoname/p/15839927.html