浮点数原理,以及跨平台计算会误差的原因分析

首先介绍下浮点数的构成以及精度等,然后最后再介绍产生误差的原因

float构成

浮点数构成:符号位、指数部分、尾数部分,符号位S占用1位,指数E占8位,尾数M占23位

float存储方式

首先我们知道常用科学计数法是将所有的数字转换成 ( ± ) a . b × 1 0 c (±)a.b\times10^c (±)a.b×10c的形式,其中a的范围是1到9共9个整数,b是小数点后的所有数字,c是10的指数。而计算机中存储的都是二进制数据,所以float存储的数字都要先转化成 ( ± ) a . b × 2 c (±)a.b \times 2^c (±)a.b×2c,由于二进制中最大的数字就是1,所以表示法可以写成 ( ± ) 1. b × 2 c (±)1.b \times2^c (±)1.b×2c的形式,float要想存储小数就只需要存储(±),b和c就可以了。float的存储正是将4字节32位划分为了3部分来分别存储正负号,小数部分和指数部分的:

  1. Sign(1位):用来表示浮点数是正数还是负数,0表示正数,1表示负数。
  2. Exponent(8位):指数部分。即上文提到数字c,但是这里不是直接存储c,为了同时表示正负指数以及他们的大小顺序,这里实际存储的是c+127。
  3. Mantissa(23位):尾数部分。也就是上文中提到的数字b。

float存储示例

以数字6.5为例,看一下这个数字是怎么存储在float变量中的:

  1. 先来看整数部分,模2求余可以得到二进制表示为110。
  2. 再来看小数部分,乘2取整可以得到二进制表示为.1(如果你不知道怎样求小数的二进制,请主动搜索一下)。
  3. 拼接在一起得到110.1然后写成类似于科学计数法的样子,得到 1.101 × 2 2 1.101 \times 2^2 1.101×22
  4. 从上面的公式中可以知道符号为正,尾数是101,指数是2。
  5. 符号为正,那么第一位填0,指数是2,加上偏移量127等于129,二进制表示为10000001,填到2-9位,剩下的尾数101填到尾数位上即可

float范围

明白了上面的原理就可求float类型的范围了,找到所能表示的最大值,然后将符号为置为1变成负数就是最小值,要想表示的值最大肯定是尾数最大并且指数最大,
那么可以得到尾数为 0.1111111 11111111 11111111,指数为 11111111,但是指数全为1时有其特殊用途,所以指数最大为 11111110,指数减去127得到127,所以最大的数字就是 1.1111111111111111111111 × 2 127 1.1111111 1111111 11111111 \times 2^{127} 1.1111111111111111111111×2127
,这个值为 340282346638528859811704183484516925440,通常表示成 3.4028235E38,那么float的范围就出来了: [-3.4028235E38, 3.4028235E38]

float精度

float 类型的数据精度取决于尾数,相信大家都知道这一点,但是精度怎么算我也是迷糊了好久,最近在不断尝试的过程中渐渐的明白了,首先是在不考虑指数的情况下23位尾数能表示的范围是 [ 0 , 2 23 − 1 ] [0, 2^{23} −1] [0,2231],实际上尾数位前面还隐含了一个"1",所以应该是一共24位数字,所能表示的范围是 [ 0 , 2 24 − 1 ] [0, 2^{24} − 1] [0,2241](因为隐含位默认是"1",所以表示的数最小是1不是0,但是先不考虑0,后面会特殊介绍,这里只按一般值计算),看到这里我们知道这24位能表示的最大数字为 2 24 − 1 2^{24}-1 2241,换算成10进制就是16777215,那么[0, 16777215]都是能精确表示的,因为他们都能写成 1. b × 2 c 1.b\times2^c 1.b×2c的形式,只要配合调整指数c就可以了。

16777215 这个数字可以写成 1.1111111111111111111111 × 2 23 1.1111111 11111111 1111111 \times 2^{23} 1.1111111111111111111111×223,所以这个数可以精确表示,然后考虑更大的数16777216,因为正好是2的整数次幂,可以表示 1.00000000000000000000000 × 2 24 1.0000000 00000000 00000000 \times 2^{24} 1.00000000000000000000000×224,所以这个数也可以精确表示,在考虑更大的数字16777217,这个数字如果写成上面的表示方法应该是 1.000000000000000000000001 ∗ 2 24 1.0000000 00000000 00000000 1 * 2^{24} 1.000000000000000000000001224,但是这时你会发现,小数点后尾数位已经是24位了,23位的存储空间已经无法精确存储,这时浮点数的精度问题也就是出现了。

看到这里发现 16777216 貌似是一个边界,超过这个数的数字开始不能精确表示了,那是不是所有大于16777216的数字都不能精确表示了呢?其实不是的,比如数字 33554432 就可以就可以精确表示成 1.00000000000000000000000 ∗ 2 25 1.0000000 00000000 00000000 * 2^{25} 1.00000000000000000000000225,说道这里结合上面提到的float的内存表示方式,我们可以得出大于 16777216 的数字(不超上限),只要可以表示成小于24个2的n次幂相加,并且每个n之间的差值小于24就能够精确表示。换句话来说所有大于 16777216 的合理数字,都是[0, 16777215]范围内的精确数字通过乘以 2 n 2^n 2n 得到的,同理所有小于1的正数,也都是 [0, 16777215] 范围内的精确数字通过乘以 2 n 2^n 2n得到的,只不过n取负数就可以了。

16777216 已经被证实是一个边界,小于这个数的整数都可以精确表示,表示成科学技术法就是 1.6777216 × 1 0 7 1.6777216 \times 10^{7} 1.6777216×107,从这里可以看出一共8位有效数字,由于最高位最大为1不能保证所有情况,所以最少能保证7位有效数字是准确的(本来是有8位,但是第一位只能是1,比如出现26777216时,就是不精确的,所以说只能保证7位),这也就是常说float类型数据的精度。

float小数

从上面的分析我们已经知道,float可表示超过16777216范围的数字是跳跃的,同时float所能表示的小数也都是跳跃的,这些小数也必须能写成2的n次幂相加才可以,比如0.5、0.25、0.125…以及这些数字的和,像5.2这样的数字使用float类型是没办法精确存储的,5.2的二进制表示为101.0011001100110011001100110011……最后的0011无限循环下去,但是float最多能存储23位尾数,那么计算机存储的5.2应该是101.001100110011001100110,也就是数字 5.19999980926513671875,计算机使用这个最接近5.2的数来表示5.2。关于小数的精度与刚才的分析是一致的,当第8位有效数字发生变化时,float可能已经无法察觉到这种变化了。

不同平台浮点数计算误差产生的原因

浮点数的精度有限,以32位的为例:
在进行浮点数运算时,例如乘法,有些平台会有FPU(浮点处理单元)进行处理有可能会使得计算结果
更精准,比如:在32位系统下,FPU使用80位的寄存器进行运算,而非FPU则使用的是SSE,虽然有128位寄存器,但是只用了其中的32位(计算过程中使用32位进行运算),所以在计算的过程中会截取位32位进行计算,导致会有误差。

所以不同的平台,使用不同的浮点优化逻辑,会导致同样的输入,会有不一样的输出,例如C#中80838.0f * -2499.0f的计算结果,linux32位的计算结果是 -202014162,而在windows32/64的计算结果是-202014160据说是Linux 32位下(Ubuntu 12.04+ gcc 4.6.3),待验证验证)

总结:

浮点运算标准IEEE-754 推荐标准实现者提供浮点可扩展精度格式,Intel x86处理器有FPU(float point unit)浮点运算处理器支持这种扩展,其他处理器就不一定支持这种扩展;换句话说,就是处理器提供提供浮点可扩展精度格式时,计算的结果可以超出定义类型的精度,例如上面提到的 80838.0f * -2499.0f的计算结果是 -202014162,-202014162这个值在32位系统下是不能用float表示的!

参考文章:

float的精度和取值范围
一个由跨平台产生的浮点数bug | 有你意想不到的结果

猜你喜欢

转载自blog.csdn.net/qq_41841073/article/details/127057494