我们知道,单精度浮点数运算时会产生误差,但是误差具体是如何产生的?本篇博文将详细分析float浮点数在运算时产生误差的原因,我们首先通过一段C代码来进行浮点运算
C代码
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#define MYDBG
int main() {
float a = 1;
float s;
float *x = NULL;
int n = 0;
int i;
float t;
while(1) {
printf("\n输入种子数 a (将用 a×2^(i-n/2) 填充数组x[n]):");
scanf("%f", &a);
printf("\na=%f\n", a);
printf("\n输入数组 float x[n] 的大小 n (非正数将退出程序):");
while(scanf("%d", &n) != 1) {
getchar();
}
if(n<1) break;
x = (float*) malloc( sizeof(float) * n );
if(x==NULL) {
printf("\n申请数组 float x[%d] 空间失败!\n", n);
continue;
}
s = 0;
for(i=0; i<n; i++) {
x[i] = a * powf(2, i-n/2);
s += x[i];
#ifdef MYDBG
printf("\nx[%d] = %f", i, x[i]);
printf(" s=%f", s);
#endif
}
printf("\n\n从 0 到 %d 累加 x[i] 得到:%f\n", n-1, s);
s = 0;
for(i=n-1; i>=0; i--) {
s += x[i];
#ifdef MYDBG
printf("\nx[%d] = %f", i, x[i]);
printf(" s=%f", s);
#endif
}
printf("\n从 %d 到 0 累加 x[i] 得到:%f\n", n-1, s);
s = 0;
for(i=0; i<n/2; i++) {
t = x[i] + x[n-1-i];
#ifdef MYDBG
printf("\nt=x[%d]+x[%d]=%f", i, n-1-i, t);
#endif
s += t;
#ifdef MYDBG
printf(" s=%f", s);
#endif
}
if(n%2) {
s += x[n/2];
#ifdef MYDBG
printf("\ns=%f after adding x[%d]", s, n/2);
#endif
}
printf("\n从 0 到 %d 成对累加 x[i], x[n-1-i] 得到:%f\n", n/2, s);
free(x);
x = NULL;
}
return 0;
}
输入种子数1和数组大小n,得到3种方式进行浮点数累加运算的结果
从0-29进行累加,可以看到在进行s=511.999969+x[24]的运算时,原本应该得到1023.999969的值,但是却得到了1024
原因如下:
我们先看看这组运算的上一组,255.999969+256=511.999969的运算过程,在计算机中,浮点是以如下结构进行表示的
sign表示的是浮点数的符号位,1表示负数,0表示整数;Expoment表示的是指数域,一个有8位;最后是一个23位宽的尾数域。
所以,二进制的表示值用上述结构表示(-1)^sign × (1+0.Mantissa) × 2^(Expoment-127)其中127是单精度浮点数的偏移量。
根据以上规则,可以得到255.999969在计算机中的表示:
符号位为0,指数为1000 0110,即十进制的134,尾数为1111 1111 1111 1111 1111 110
表示的二进制数为1.11111111111111111111110 × 2^7
同理可得256
表示的二进制为1×2^8
接着是进行2个浮点数的相加,浮点数的相加规则是将指数较小的数进行小数点左移使两者指数相等,再进行浮点数的相加,最后进行规范化处理使得能存储在计算机中
根据规则,在进行相加时,需要将1.11111111111111111111110 × 2^7变为0.111111111111111111111110 × 2^8
相加得1.11111111111111111111111 × 2^8,再将这个数化为十进制,即为511.999969,没有产生误差。
看产生误差的一组运算 511.999969
表示的二进制数为1.11111111111111111111111 × 2^8
512
表示的二进制数为2^9
根据规则,在进行相加时,需要将1.11111111111111111111111 × 2^8变为0.111111111111111111111111 × 2^9
相加得1.111111111111111111111111× 2^9,此时出现了一个问题,尾数有24位!可是我们计算机中的存储结构中尾数只有23位,怎么办呢?这里,如果第24位是1,则会进行向前进位的操作,最后保留23位。
0.111111111111111111111111->1.00000000000000000000000
即(1+1)×2^9 = 2^10 = 1024,这个进位误差使得浮点数的相加运算产生了误差
但是不是所有因为存储空间不足导致的运算都会产生误差呢,答案是 不是的
下面来看另一组累加15.999969+16=31.999969
15.999969
16
15.999969表示的为1.11111111111111111011111 ×2^3,16表示的为2^4
根据规则,在进行相加时,需要将1.11111111111111111011111 × 2^3变为0.111111111111111111011111 × 2^4
相加得1.111111111111111111011111 × 2^4,尾数同样有24位,对尾数进行进位保留23位得
1.11111111111111111110000 × 2^4,即十进制的31.999969,没有产生误差。
接着看从29-0进行累加
其中产生误差的原因与前面分析的原因类似,就不在具体阐述,有兴趣的童鞋可以自己试试分析
最后看一组首尾相加再累加
惊奇地发现,最后得到的结果和前面2种方法得到的结果不一致,看过程,发现第一步的运算直接把小数部分忽略了
0.000031 = 1.00000100000010111111111 × 2^-15
16384 = 2^14
可以看到,二者指数位差了29,所以进行浮点数相加运算时,需要移位29位,但是尾数只有23位,移位会导致数据全部丢失,导致16384+0.000031相等于是16384+0=16384,可见当2个浮点数相差过大时,会产生很大的误差!
以上所述的便是浮点数在进行运算时因为规范化处理导致存储位数不够而导致的误差,但是这个误差并不是浮点数运算的根本误差,浮点数运算的根本误差在于,十进制并不能精确地转换为二进制,但是二进制却能精确地转换为十进制
如0.6用二进制表示0.100110011001100110011……这是一个1001循环的无限小数,计算机只会取其23位作为尾数,还没开始算呢,误差就产生了,这便是浮点数运算产生误差的根本原因