C语言中的浮点数,满足IEEE754标准,可表示带有小数的数值,C语言中的浮点数有精度限制,并不能表示所有的数。
IEEE754单精度浮点数:
表示的数的10进制的有效位数只有7位,其中一位是符号位,8位阶码,23位尾数。在实数轴上,同一个阶码范围内,能够表示 个不同的数字。所以并不是所有在表示范围内的数都可以准确表示。
以下我们用一个简单的C语言程序test.c来进一步了解浮点数的精度问题。
#include <stdio.h>
void main()
{
float tem[10];
float a = 123456789;
int* pTem;
int i;
pTem = (int*)tem;
tem[0]=61.419996;
tem[1]=61.419997;
tem[2]=61.419998;
tem[3]=61.419999;
tem[4]=61.419990;
tem[5]=61.420001;
tem[6]=61.420002;
tem[7]=61.420003;
tem[8]=61.420004;
tem[9]=61.420005;
for (int i=0;i<10;i++) {
printf("%.6f,0x%x\n",tem[i],*(pTem+i));
}
printf("%f\n",a);
return;
}
利用gcc命令编译查看运行结果:
gcc -o0 -m32 -g test.c -o test
./test
运行结果:
61.419994,0x4275ae13
61.419998,0x4275ae14
61.419998,0x4275ae14
61.419998,0x4275ae14
61.419991,0x4275ae12
61.420002,0x4275ae15
61.420002,0x4275ae15
61.420002,0x4275ae15
61.420006,0x4275ae16
61.420006,0x4275ae16
123456792.000000
由输出结果我们可以看出,只有tem[2]和tem[6]这两个数,也就是61.419998和61.420002能准确的表示,其他8个数的输出都有了一定的偏差,也就是说在机器内部并不能表示这8个数,编译器在编译时将它们就近截断到了机器能表示的浮点数,这一点从它们的机器数上也能看的出来。
这10个数在机器中只有四种不同的表示。根据浮点数的表示方式可以知道,这四个机器数的符号,阶码都相同,只是尾数依次增加了1,这进一步说明了单精度浮点数只能表示7位十进制有效数,第8位是不准确的。其实61.419998和61.420002这两个数也不能准确表示,只是在printf语句输出时进行了舍入,得到了看上去准确的结果。
再看最后一行输出,单精度浮点数也不能表示值为123456789的浮点数,它会把截断为123456792。
累加操作
在浮点运算问题中,我们常常会遇到累加操作,我们要注意大数吃小数的问题。
我们仍然以下面这样一个简单的C语言累加四百万个0.1作为介绍例子:
#include <stdio.h>
void main()
{
int i;
float tem,sum;
tem=0.1;
sum=0;
for (i=0;i<4000000;i++) {
sum += tem;
}
printf("%f\n",sum);
return;
}
输出结果为: 384524.781250,结果并不等于400000.
结果不等于40万的原因:
IEEE754的单精度浮点数并不能精确地表示0.1
存在大数吃小数的问题
大数吃小数问题
问题由来: 进行浮点数加减操作时,需要先进行对阶操作。
对阶操作: 将两个操作数的阶码统一,执行小阶向大阶看齐,阶码小的数的尾数右移动,右移尾数等于两个阶码差的绝对值。
导致后果: 小数的部分尾数丢失,损失精度。
KAHAN累加算法
既然浮点数的累加有精度问题,我们怎么来减小这个误差呢,答案就是利用KAHAN算法。这个算法是1989年度的图灵奖获得者,浮点数之父William Kahan提出的。
主要思想: 设法计算出每次累加所带来的舍入误差,并将其添加在下一次的加数上,这样就可以获得更准确的结果。
使用前提: 尽量在浮点数数值相近时进行加减计算,才能充分利用有效位数。
验证kahan算法有效性的代码示例:
#include <stdio.h>
void main()
{
float sum=0;
float sum1=0;
float c=0;//累加产生的误差
float y,t;//y是经过误差修正后的加数,t是经过本次累加后的和
int i;
for(i=0;i<4000000;i++)
{
sum1 += 0.1;
}
//kahan算法实现-->
for(i=0;i<4000000;i++)
{
y=0.1-c;
t=sum+y;
c=(t-sum)-y;//(t-sum)是本次累加实际加上的加数
sum=t;
}
printf("sum1=%f\n",sum1);
printf("sum=%f\n",sum);
return;
}
以上代码我们采用了两种算法来进行计算,一种是直接累加,一种是采用kahan算法累加。
运行结果如下:
sum1=384524.781250
sum=400000.000000
可以看到利用kahan算法可以得到更为准确的值,当然kahan算法的代价是原来每步只需要一次加法操作和一次赋值操作,而kahan算法要需要四次加减法操作和四次赋值操作,但是这个代价是值得的。
下面来重点分析以下机器指令如何来实现这个过程,以下是反汇编的指令代码。
0000051d <main>:
#include <stdio.h>
void main()
{
51d: 8d 4c 24 04 lea 0x4(%esp),%ecx
521: 83 e4 f0 and $0xfffffff0,%esp
524: ff 71 fc pushl -0x4(%ecx)
527: 55 push %ebp
528: 89 e5 mov %esp,%ebp
52a: 53 push %ebx
52b: 51 push %ecx
52c: 83 ec 30 sub $0x30,%esp
52f: e8 ec fe ff ff call 420 <__x86.get_pc_thunk.bx>
534: 81 c3 a4 1a 00 00 add $0x1aa4,%ebx
float sum=0;
53a: d9 ee fldz
53c: d9 5d e0 fstps -0x20(%ebp)
float sum1=0;
53f: d9 ee fldz
541: d9 5d e4 fstps -0x1c(%ebp)
float c=0;
544: d9 ee fldz
546: d9 5d e8 fstps -0x18(%ebp)
float y,t;
int i;
for(i=0;i<4000000;i++)
549: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%ebp)
550: eb 12 jmp 564 <main+0x47>
{
sum1 += 0.1;
552: d9 45 e4 flds -0x1c(%ebp)
555: dd 83 b0 e6 ff ff fldl -0x1950(%ebx)
55b: de c1 faddp %st,%st(1)
55d: d9 5d e4 fstps -0x1c(%ebp)
for(i=0;i<4000000;i++)
560: 83 45 ec 01 addl $0x1,-0x14(%ebp)
564: 81 7d ec ff 08 3d 00 cmpl $0x3d08ff,-0x14(%ebp)
56b: 7e e5 jle 552 <main+0x35>
}
for(i=0;i<4000000;i++)
56d: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%ebp)
574: eb 2d jmp 5a3 <main+0x86>
{
y=0.1-c;
576: d9 45 e8 flds -0x18(%ebp)
579: dd 83 b0 e6 ff ff fldl -0x1950(%ebx)
57f: de e1 fsubp %st,%st(1)
581: d9 5d f0 fstps -0x10(%ebp)
t=sum+y;
584: d9 45 e0 flds -0x20(%ebp)
587: d8 45 f0 fadds -0x10(%ebp)
58a: d9 5d f4 fstps -0xc(%ebp)
c=(t-sum)-y;
58d: d9 45 f4 flds -0xc(%ebp)
590: d8 65 e0 fsubs -0x20(%ebp)
593: d8 65 f0 fsubs -0x10(%ebp)
596: d9 5d e8 fstps -0x18(%ebp)
sum=t;
599: d9 45 f4 flds -0xc(%ebp)
59c: d9 5d e0 fstps -0x20(%ebp)
for(i=0;i<4000000;i++)
59f: 83 45 ec 01 addl $0x1,-0x14(%ebp)
5a3: 81 7d ec ff 08 3d 00 cmpl $0x3d08ff,-0x14(%ebp)
5aa: 7e ca jle 576 <main+0x59>
}
printf("sum1=%f\n",sum1);
5ac: d9 45 e4 flds -0x1c(%ebp)
5af: 83 ec 04 sub $0x4,%esp
5b2: 8d 64 24 f8 lea -0x8(%esp),%esp
5b6: dd 1c 24 fstpl (%esp)
5b9: 8d 83 98 e6 ff ff lea -0x1968(%ebx),%eax
5bf: 50 push %eax
5c0: e8 eb fd ff ff call 3b0 <printf@plt>
5c5: 83 c4 10 add $0x10,%esp
printf("sum=%f\n",sum);
5c8: d9 45 e0 flds -0x20(%ebp)
5cb: 83 ec 04 sub $0x4,%esp
5ce: 8d 64 24 f8 lea -0x8(%esp),%esp
5d2: dd 1c 24 fstpl (%esp)
5d5: 8d 83 a1 e6 ff ff lea -0x195f(%ebx),%eax
5db: 50 push %eax
5dc: e8 cf fd ff ff call 3b0 <printf@plt>
5e1: 83 c4 10 add $0x10,%esp
return;
5e4: 90 nop
}
直接累加方法分析
int i;
for(i=0;i<4000000;i++)
549: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%ebp)
550: eb 12 jmp 564 <main+0x47>
{
sum1 += 0.1;
552: d9 45 e4 flds -0x1c(%ebp)
555: dd 83 b0 e6 ff ff fldl -0x1950(%ebx)
55b: de c1 faddp %st,%st(1)
55d: d9 5d e4 fstps -0x1c(%ebp)
for(i=0;i<4000000;i++)
560: 83 45 ec 01 addl $0x1,-0x14(%ebp)
564: 81 7d ec ff 08 3d 00 cmpl $0x3d08ff,-0x14(%ebp)
56b: 7e e5 jle 552 <main+0x35>
}
首先把0送到地址ebp -0x14里,就是i的地址,就执行了i=0这一步操作无条件转到564这一行,也就是执行命令cmpl $0x3d08ff,-0x14(%ebp),将变量i和4000000比较,如果i小于4000000,则跳转到552这个位置,把地址为ebp -0x1c内存单元里的单精度数 sum 和ebp -0x1950里的双精度数 0.1 入浮点数栈st0和st1进行相加运算。相加结果按单精度数出栈保存到ebp -0x1c内存单元 sum,然后把地址为ebp -0x14的数加上1(i++这一语句实现),最后继续循环执行cmpl $0x3d08ff,-0x14(%ebp) 命令。直到i不小于4000000。
kahan算法分析
for(i=0;i<4000000;i++)
56d: c7 45 ec 00 00 00 00 movl $0x0,-0x14(%ebp)
574: eb 2d jmp 5a3 <main+0x86>
{
y=0.1-c;
576: d9 45 e8 flds -0x18(%ebp)
579: dd 83 b0 e6 ff ff fldl -0x1950(%ebx)
57f: de e1 fsubp %st,%st(1)
581: d9 5d f0 fstps -0x10(%ebp)
t=sum+y;
584: d9 45 e0 flds -0x20(%ebp)
587: d8 45 f0 fadds -0x10(%ebp)
58a: d9 5d f4 fstps -0xc(%ebp)
c=(t-sum)-y;
58d: d9 45 f4 flds -0xc(%ebp)
590: d8 65 e0 fsubs -0x20(%ebp)
593: d8 65 f0 fsubs -0x10(%ebp)
596: d9 5d e8 fstps -0x18(%ebp)
sum=t;
599: d9 45 f4 flds -0xc(%ebp)
59c: d9 5d e0 fstps -0x20(%ebp)
for(i=0;i<4000000;i++)
59f: 83 45 ec 01 addl $0x1,-0x14(%ebp)
5a3: 81 7d ec ff 08 3d 00 cmpl $0x3d08ff,-0x14(%ebp)
5aa: 7e ca jle 576 <main+0x59>
}
前面几句命令就不解释了,跟上面的直接累加方法分析一样的都是执行for循环。重点分步分析循环内部 kahan 算法的实现语句。
y=0.1-c;
576: d9 45 e8 flds -0x18(%ebp)
579: dd 83 b0 e6 ff ff fldl -0x1950(%ebx)
57f: de e1 fsubp %st,%st(1)
581: d9 5d f0 fstps -0x10(%ebp)
把地址为ebp -0x18内存单元里的单精度数 c 和ebp -0x1950里的双精度数 0.1 入浮点数栈st0和st1进行相减运算。相减结果按单精度数出栈保存到ebp -0x10内存单元 (y)。
c=(t-sum)-y;
58d: d9 45 f4 flds -0xc(%ebp)
590: d8 65 e0 fsubs -0x20(%ebp)
593: d8 65 f0 fsubs -0x10(%ebp)
596: d9 5d e8 fstps -0x18(%ebp)
把地址为edp -0xc内存单元里的单精度数 t 减去ebp -0x20里的单精度数 (sum) ,再减去ebp -0x10里的单精度数 y 。相减结果按单精度数出栈保存到ebp -0x18内存单元 ©。
printf("sum=%f\n",sum);
5c8: d9 45 e0 flds -0x20(%ebp)
5cb: 83 ec 04 sub $0x4,%esp
5ce: 8d 64 24 f8 lea -0x8(%esp),%esp
5d2: dd 1c 24 fstpl (%esp)
5d5: 8d 83 a1 e6 ff ff lea -0x195f(%ebx),%eax
5db: 50 push %eax
5dc: e8 cf fd ff ff call 3b0 <printf@plt>
5e1: 83 c4 10 add $0x10,%esp
把地址为edp -0x20内存单元里的单精度数 sum 减去4并存入到寄存器esp -0x8地址,
以上就是小编本次给大家分享的计算机系统学习笔记-浮点数的精度问题。经过了不断的查资料,才理解了这一部分的指令,希望对大家有所帮助。