IEEE754浮点数标准及浮点型和整型之间的转换

本文详细介绍了IEEE754浮点数标准,包括浮点数的表示方式、移码、非规约形式、舍入规则、有效数字以及不同精度浮点数的取值范围。此外,还探讨了C/C++和Java中浮点型到整型转换的区别,以及如何正确使用pow()函数避免精度问题。

IEEE二进制浮点数算术标准IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU浮点运算器所采用。这个标准定义了表示浮点数的格式(包括负零-0)与反常值(denormal number),一些特殊数值((无穷(Inf)与非数值(NaN)),以及这些数值的“浮点数运算符”;它也指明了四种数值舍入规则和五种例外状况(包括例外发生的时机与处理方式)。

IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。只有32位模式有强制要求,其他都是选择性的。大部分编程语言都提供了IEEE浮点数格式与算术,但有些将其列为非必需的。例如,IEEE 754问世之前就有的C语言,现在包括了IEEE算术,但不算作强制要求(C语言的float通常是指IEEE单精确度,而double是指双精确度)。

该标准的全称为IEEE二进制浮点数算术标准(ANSI/IEEE Std 754-1985),又称IEC 60559:1989,微处理器系统的二进制浮点数算术(本来的编号是IEC 559:1989)[1]。后来还有“与基数无关的浮点数”的“IEEE 854-1987标准”,有规定基数为2跟10的状况。现在最新标准是“ISO/IEC/IEEE FDIS 60559:2010”。

在六、七十年代,各家计算机公司的各个型号的计算机,有着千差万别的浮点数表示,却没有一个业界通用的标准。这给数据交换、计算机协同工作造成了极大不便。IEEE的浮点数专业小组于七十年代末期开始酝酿浮点数的标准。在1980年,英特尔公司就推出了单片的8087浮点数协处理器,其浮点数表示法及定义的运算具有足够的合理性、先进性,被IEEE采用作为浮点数的标准,于1985年发布。而在此前,这一标准的内容已在八十年代初期被各计算机公司广泛采用,成了事实上的业界工业标准加州大学伯克利分校的数值计算与计算机科学教授威廉·卡韩被誉为“浮点数之父”。”     ——摘自维基百科

IEEE754标准包含一组实数的二进制表示法。它有三部分组成:

  • 符号位
  • 指数位/阶码
  • 尾数位

三种精度的浮点数各个部分位数如下:

规约形式的浮点数

如果浮点数中指数部分的编码值在 0< exponent ≤ 2^e-2 之间,且在科学表示法的表示方式下,尾数部分最高有效位(即整数字)是1,那么这个浮点数将被称为规约形式的浮点数。“规约”是指用唯一确定的浮点形式去表示一个值。

对于将某个实数表示为计算机浮点数,首先要将其正规化,也就是表示为形如:



的样子。其中b01,而p二进制数表示的指数位。这样,假设想表示为单精度(float)的浮点数,那么:

  • 第一位符号位用0表示正,用1表示负
  • 将指数p加上移码表示为8位的二进制数
  • 在接下来的23位填充位数b部分。由于正规化表示时,最左边部分总是1,所以我们只需表示23位的尾数即可。

移码

上述中有一个词:移码(exponential bias)。因为指数p有正有负,那么在8位的指数位中我们就要拿出第一位来指示符号,这样显然会造成不必要的浪费。给指数加上移码,就能保证结果总是一个非负数,也就可以将8个指数位都利用起来。对于有M个指数位的精度,其移码为:

这样就得到上面三种精度的移码:

以单精度(float)的为例。双精度的指数位有8位。这样可以表示的数是从0000 00001111 1111,也就是指数加移码所表示的范围从0到255,那么,减去移码127,则可以表示的指数是-127到128。但是注意,-127和128作为他用(后面会说到)。所以实际上能表示数的指数是从-126到127。

例子

【例】:求3.14的单精度浮点数表示。
首先将3.14转成二进制:

  • 整数部分3的二进制是11b
  • 小数部分0.14的二进制是:0.0010001111010111000010[10001111.....]b(方括号中表示小数点后第23位及之后)。

这样,3.14的二进制代码就是:11.0010001111010111000010[10001111....]×2^0b(方括号中表示小数点后第23位及之后),那么用正规化表示就是:1.10010001111010111000010[10001111....]×2^1b(方括号中表示小数点后第24位及之后)。由于单精度浮点数尾数只有23位,所以需要舍入(舍入方法见后):由于第24位为1,且之后 不全为 0,所以需要向第23位进1完成上舍入:1.10010001111010111000011×2^1b
而其指数是1,需要加上移码127,即128,也就是(1000 0000)b
它又是正数,所以符号为0。
综上所述,3.14的单精度浮点数表示为:
0 1000-0000 1001-0001-1110-1011-1000-011b
十六进制代码为:0x4048F5C3

通过此例可知,3.14的单精度浮点数表示是0 1000-0000 1001-0001-1110-1011-1000-011。现在我们来还原,看看它的误差:

  • 指数是128,那么还原回去(减去移码),实际指数就是1
  • 尾数还原也就是:10010001111010111000011b,所以是:1.10010001111010111000011×2^1b,也就是11.0010001111010111000011b

利用二进制转十进制,可得它对应的十进制数是:3.1400001049041748046875。显然与3.14是有误差的。

我们再通过另一种方法估算误差。从例子中可知,对于3.14的单精度浮点数,我们舍去了第24位以及之后,它们是:
0.00...(23个0) [10001111.....]×2^1b
为了方便计算,不妨假设此后全是0(即方括号中省略部分),也就是舍去了:
0.10001111b×2^(-23)×2^1b
约为0.00000013317912817001;由于舍入进位关系,给第23位又加了1,所以加了:2^(-23)×2^1,故而要减去这一部分。
所以,误差约为2^(-23)×2^1 - 0.10001111b×2^(-23)×2^1=0.00000010523945093155。所以结果大致为3.14+0.00000010523945093155=3.14000010523945093155
可见和上面计算结果大致相同。

注:对于float,2^24-1=16777215,即[0, 16777215]范围的整数都可以被精确表示。16777215可以表示为:

1.1111111 11111111 11111111*2^23,然后考虑16777216,可以表示为:1.0000000 00000000 00000000*2^24,所以这个数也可以被精确表示(可以理解为恰好能被精确表示);再考虑16777217,表示为:1.0000000 00000000 000000001*2^24,这时你会发现,小数点后已经是24位了,23位的存储空间已经无法精确存储了。

所以对于单精度float,考虑到负数,[-16777216, 16777216] 范围内的整数都可以被精确表示。
 

非规约形式的浮点数

如果浮点数的指数部分的编码值是0,分数部分非零,那么这个浮点数将被称为非规约形式的浮点数。一般是某个数字相当接近零时才会使用非规约型式来表示。 IEEE 754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值小1。例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0。

特殊值

这里有三个特殊值需要指出:

  1. 如果指数是0并且尾数的小数部分是0,这个数±0(和符号位相关)
  2. 如果指数 =  2^e-1并且尾数的小数部分是0,这个数是±(同样和符号位相关)
  3. 如果指数 = 2^e-1并且尾数的小数部分非0,这个数表示为非数(NaN)

以上规则,总结如下:

形式指数尾数部分
 00
非规约形式 0大于0小于1
规约形式 1到 2^e-2大于等于1小于2
无穷 2^e-10
NaN 2^e-1非0

舍入规则

任何有效数上的运算结果,通常都存放在较长的寄存器中,当结果被放回浮点格式时,必须将多出来的比特丢弃。 有多种方法可以用来运行舍入作业,实际上IEEE标准列出4种不同的方法:

  • 舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Roundings to nearest,ties To Even,也叫Round half to even,这是默认的舍入方式):会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中是以0结尾的)。
  • 朝+∞方向舍入:会将结果朝正无限大的方向舍入。
  • 朝-∞方向舍入:会将结果朝负无限大的方向舍入。
  • 朝0方向舍入:会将结果朝0的方向舍入。

如果以形式1.RR..RDD..D表示浮点数(R表示有效位,或保留位,而D表示舍去位),默认的Roundings to nearest even舍入规则就是:

  • 如果DD..D < 10..0,则向下舍入
  • 如果DD..D > 10..0,则向上舍入
  • 如要DD..D = 10..0,则向最近偶数舍入,细则如下 :

    a. 如果RR..R = XX..0 (X表示任意值,0或1),则向下舍入

    b. 如果RR..R = XX..1,则向上舍入

通俗来讲,以23位尾数位的单精度浮点数为例,舍入时需要重点参考第24位:

  • 若第24位为1,而第24位之后全部为0。此时就要使第23位为0,分2种情况:

     1. 若第23位本来就是0则不管。

     2. 若第23位为1,则第24位就要向第23位进1位,这样第23位就可以为0。

  • 若不是上面的情况,即第24位1,但是第24位之后不全为0,则第24位就要向第23位进1位完成上舍入。
  • 若都不是上面两种情况,那么第24位必为0,此时直接舍去不进位,称为下舍入。

有效数字

单精和双精浮点数的有效数字分别是有存储的23和52个位,加上最左手边没有存储的第1个位,即是24和53个位。

由以上的计算,单精和双精浮点数可以保证7位和15位十进制有效数字。

注意:整数部分和小数部分一共是7位和15位,而不是小数点之后7位和15位。

举个例子:

#include <iostream> 
using namespace std;

int main() {
	float f1 = 0.1f;
	float f2 = 123.56789f;
	printf("f1 = %f\n", f1);
	printf("f1 = %.30f\n", f1);
	printf("f2 = %f\n", f2);
	printf("f2 = %.30f\n", f2);

	return 0;
}

输出:

十进制0.1表示为二进制为:0.000110011001100110011001100110011001100110011001100...(无限循环)

根据IEEE754及默认舍入规则,在内存中实际存储为:0x3dcccccd,表示为十进制:0.100000001490116119384765625

%f 默认打印小数点后6位,但0.1f的在内存中存的真正的十进制值是:0.100000001490116119384765625

除了调试模式看实际内存值外,这个IEEE754转换网站也很方便(IEEE-754 Floating Point Converter)。

同理,123.56789f 在内存中实际存储为:0x42f722c2,可看出输出123.5678这7位是有效的。

补充:既然0.1f不能精确存储,为什么Java里面可以打印出精确的0.1呢?Java System.out.println是如何输出浮点型的?

查看Java 8 Float.toString()方法的API文档:Float (Java Platform SE 8 )

大概意思就是:小数部分至少1位,并且小数部分要用尽量少的位数来唯一区分相邻的两个Float数值。

对于0.1f来说,上一个float值为0.099999994,下一个float值为0.10000001,所以只需要0.1就能从相邻浮点数之间精确表达。

上下float值可以通过Math.nextDown()和Math.nextUp()方法取得,由于不知道Intellij idea 怎么可以查看到浮点型变量的内存实际存储,所以使用Integer.toHexString(Float.floatToRawIntBits())方法来打印。

public class Main {
    public static void main(String[] args) {
        float f = 0.1f;
        float nextdownf = Math.nextDown(f);
        float nextupf = Math.nextUp(f);

        System.out.println("f = " + f);
        System.out.println("nextdownf = " + nextdownf);
        System.out.println("nextupf = " + nextupf);
        System.out.println(Integer.toHexString(Float.floatToRawIntBits(f)));
        System.out.println(Integer.toHexString(Float.floatToRawIntBits(nextdownf)));
        System.out.println(Integer.toHexString(Float.floatToRawIntBits(nextupf)));
    }
}

输出:

可看出这0.1f 内存中的实际存储及相邻上下的float存储,二进制刚好相差1。

取值范围

单精度:[-3.4*10^38, -1.18*10^-38] U [1.18*10^-38, 3.4*10^38]

双精度:[-1.80*10^308, -2.23*10^-308] U [2.23*10^-308, 1.80*10^308]

C/C++和Java浮点型到整型的转换之差异

先看例子:

C/C++:

#include <iostream> 
using namespace std;

int main() {
	int a = 0x7fffffff;
	float f1 = a;
	int i = f1;
	printf("a = %d\n", a);
	printf("f1 = %f\n", f1);
	printf("i = %d\n", i);
	printf("a == i: %d\n", (a == i));
	
	return 0;
}

输出:

Java:

public class Main {
    public static void main(String[] args) {
        int a = 0x7fffffff;
        float b = a;
        int i = (int) b;
        long l = (long) b;
        System.out.println("a = " + a);
        System.out.println(Integer.toHexString(Float.floatToRawIntBits(b)));
        System.out.println("i = " + i);
        System.out.println("a == i: " + (a == i));
        System.out.println("l = " + l);
    }
}

输出:

根据IEEE754,整型int 0x7fffffff 转换为float,float实际存储为:0x4f000000

但是转回为int C/C++和Java却不一样了?

原因:

C/C++ float转int标准:

Java float转int标准:

可看出,根据IEEE754,整型int 0x7fffffff 转换为float,默认Round half to even舍入规则,float实际存储为:0x4f000000,再转回十进制为2147483648,但已经超过int的最大能表示值(2147483647),针对这种情况(float转换为int,但超过了int所能表示范围),C/C++是UB未定义行为(VS2015实现为将2147483648(内存中:0x8000 0000)看成无符号数再转换为int,即-2147483648),Java转换为int最大值(2147483647)。

如何正确使用库函数pow()?

C++中的函数原型:

Java中的:

可见,无论C++还是Java,都没有 int pow (int base, int exp); 实际上,标准库实现pow函数为:

pow(x,a)=exp(a*log(x))

其中exp和log分别表示以e为底的指数和对数函数。

使用专门的浮点运算单元(FPU),并不是采用简单的迭代乘法,所以在有些编译器上面,(int)pow(10,2)可能打印出99,因为计算出的值可能是99.999999,强制转换为int将舍弃小数,直接变成99了。那整型是不是就不能使用pow()函数了?不是,当你确定是结果为正整数且不超过int型最大可表示范围,(int)(pow(10, 2)+0.5),这样就会四舍五入取到正确值了。相应的,如果结果是负数,则是(int)(pow(-10, 3)-0.5)。当然,从C++11或C99起,提供了round函数,(int)round(pow(10, 2))和(int)round(pow(-10, 3))就可以了,但很多项目中代码还不是C++11或C99,考虑到移植性,不建议项目中使用round函数。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值