计算机系统基础学习笔记(3)-浮点数的精度问题

C语言中的浮点数,满足IEEE754标准,可表示带有小数的数值,C语言中的浮点数有精度限制,并不能表示所有的数。

IEEE754单精度浮点数:

表示的数的10进制的有效位数只有7位,其中一位是符号位,8位阶码,23位尾数。在实数轴上,同一个阶码范围内,能够表示 2 23 2^{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地址,
以上就是小编本次给大家分享的计算机系统学习笔记-浮点数的精度问题。经过了不断的查资料,才理解了这一部分的指令,希望对大家有所帮助。

原创文章 42 获赞 11 访问量 3341

猜你喜欢

转载自blog.csdn.net/qq_43336390/article/details/105693692