C代码的简单优化方法(机器:intel酷睿i7,摘自csapp)

【概念介绍】
1、CPE :每元素周期数,帮助我们理解循环性能。
执行一个循环用的时钟周期数 = 总时钟周期 / 元素数
我们更愿意用每个元素(循环体里面的重复执行的部分)的周期数来衡量速度,而不是每次循环的周期数。因为像循环展开,我们用更少的循环来完成计算
运行时间:368+9.0n =准备循环以及完成过程的开销是368+每个元素9个周期的线性因子

解释什么叫元素周期:
法一:
for(i = 0 ; i < n; i++){
	b[i] = a[i];
}
循环展开:
for(i = 0; i < n-1; i += 2){
	b[i] = a[i];
	b[i+1] = a[i + 1];
}
for(i; i < n ;i++){
	b[i] = a[i];
}
此时的元素数是 n 

【方法】

一、编译的不优化

1、不优化内存别名使用

这种两个指针可能指向同一内存位置的情况叫 内存别名使用
示例1:
voi twiddle1(long *x,long *y)
{
	*x += *y;
	*x += *y;
}

改进:
voi twiddle2(long *x,long *y)
{
	*x += 2 * (*y);
}
这种合并在两个指针指向同一地址会出错

2、不优化函数调用

long f();

long fun1(){
	return f()+f()+f();
}

改进:
long fun2(){
	return 4*f();
}
如果函数有副作用,比如
f(){
	全局变量 count ++;
    或者其他影响下次调用的代码
}
如果编译器像程序员这样优化就出错了,所以编译器拒绝函数调用优化;

解决办法: 内联
long fun2(){
	t += count;
	t += count;
	t += count;
	return t;
}

二、消除连续的函数调用
1、代码移动
把在循环里要执行多次,但是结果不会改变的计算,移到循环外面

for(i = 0; i < strlen(s), i++){
	if(s[i]>='A' && s[i]<='Z')
		s[i] -= ('A' - 'a');
}

改进
int len = strlen(s)for(i = 0; i < len , i++){
	if(s[i]>='A' && s[i]<='Z')
		s[i] -= ('A' - 'a');
}
在数据很大的时候这个效率差距差了近千倍
未优化的 抽象的 goto语言的版本
	i = 0;
loop:
	if(i >= strlen(s)) //这要执行多少次???
		goto end;
	if(s[i]>='A' && s[i]<='Z')
		s[i] -= ('A' - 'a');
	i ++;
	if(i < strlen(s))
		goto loop
end:

三、消除不必要的内存引用
过多的变量会存在栈中,含(指针引用)的也会存在栈中

typedef struct{
	long len;
	int *data;
}*vec_ptr;

int* get_start(vec_ptr v){
	return v->data;
}
整数求和 写法1:
传入参数 *sum{
int *data =  get_start (v);
for(i = 0; i < len ;i++){
	*sum = *sum + data[i];
}}
整数求和 写法2int acc;
for(i = 0; i < len ;i++){
	acc = acc + data[i];
}
*dest = acc;

此处的内存引用是指针的引用。
超标的寄存器变量和指针都是存在特殊的内存———栈中

四、概念补充
【1】整数加、乘和浮点数加和乘是完全流水化的
除法占有3~30、3-15浮点 个时钟周期,而且没有完全流水化····
【2】延迟界限L:完成合并运算的函数所需要的最小的CPE值
计算n个单元的时钟周期:L*n + K
请添加图片描述

五、低级优化

1. 展开循环降低开销 k * 1
【 原理】:增加每次迭代计算的元素的数量,减少循环迭代次数。
首先,减少了循环索引计算和条件分支
其次,减少整个计算中关键路径上的操作数量
【效果】对加法效果明显,对其他运算没有太大影响

//k*1循环展开,以5*1循环展开为例
for(i = 0; i < len-4; i += 5){
	a = (a + data[i])+data[i+1];
	a = (a + data[i+2])+data[i+3];
	a = a + data[i+4];
}
for(i; i < len ;i++){
	a = a + data[i];
}

2. 通过使用例如多个累计变量和重新结合等技术,找到方法提高指令的并行性
【硬件特性】
1)容量C:4个加法器件,2个浮点乘器件,其余一个
2)发射I:除了 除法 都实现了流水化
3)延迟L:整数+1,整数乘3,浮点+3,浮点5,除都很多3-30、3-15
执行加法和乘法是完全流水化的并且有多个功能单元一起
4)循环寄存器之间的操作链,决定了限制性能的数据相关
请添加图片描述
请添加图片描述
补充说明:在流水线中,一个浮点乘法要5个时钟周期
在运算里面本来是三个乘法,如果关键路径用了三个乘法就是 5
3/3;如果用了两个乘法就是5*2/3这样算那个最少的CPE

办法1】多个累积变量 k * k
1)多个累积变量:将一个运算拆成多个运算,并在最后合并结果来提高性能
2)要求 (k* k循环展开,k 最小是多少)
只有保持该循环操作的所有的功能器件都是满的,才会达到最大吞吐量
要求循环展开因子 k >= C * L
C是容量,L是延迟;
例如:浮点乘法 2*5 ,k>=10;浮点加k>=3
对整数乘法、浮点加法、浮点乘法性能约提升2倍

2*2 展开
计算前n个的乘积
for(i = 0 ; i < len-1 ;i+=2){
	acc0 = acc0 * data[i];//奇数积 循环acc0
	acc1 = acc1 * data[i+1];//偶数积 循环acc1
}
for(i ; i < len ;i++)
	acc0 = acc0 * a[i];//落单积
*dest = acc0 * acc1;//总积
内循环包括两个乘法操作,被翻译成读写不同的乘法功能单元
风险:
整数的加法和乘法是可交换、可结合的
但是浮点数的加法和乘法是不可结合的

办法2】重新结合变换 k * 1a
1)重新结合变化:
针对于乘法的k1循环展开,改变括号位置,除整数加法,都要比k1循环展开效果好
2) 优势
一个循环每次迭代两个乘法
优势就在于第一个乘法,不需要等待前一次迭代的累积值就可以执行,所以最小的CPE少了2倍
(data[i] * data[i+1]),乘法的两个数,直接从内存加载就可以了,不需要的等待乘数a

2*1
for(i = 0; i < len-1; i += 2){
	a = (a * data[i]) * data[i+1];//a是放在循环寄存器里面
}

2*1a
for(i = 0; i < len-1; i += 2){
	a = a * (data[i] * data[i+1]);//a是放在循环寄存器里面
}

注意:
如果并行度太高,并行度P超过了可用寄存器数量,编译器会将某些值放入内存中。如k=20不如k=10的效率好。
x86-64上有16个寄存器,可以使用16个浮点数寄存器,所以循环变量不要超过可用寄存器数量

3. 在条件语句中,用功能性风格使编译器采用条件数据传送,避免分支预测错误的开销
【方法】
改变编码风格,让汇编代码执行条件传送
就是先两种结果都计算,用的时候再比较条件,选择哪个是对的再输出

条件控制
for(i = 0; i < len ;i ++){
	if(a[i]>b[i]){
	    t = a[i];
		a[i] = b[i];
		b[i] = t;
	}
}

条件传送
for(i = 0; i < len ;i ++){
	min = a[i] < b[i] ? a[i]:b[i];
	max = a[i] > b[i] ? a[i]:b[i];
	a[i] = min;
	b[i] = max;
}

六、重新排列循环以提高空间局部性
1)数组按行存储,最好ijk顺寻访问,步长为1
2)一旦读入了一条数据,尽可能的多地使用它

计算C矩阵= A矩阵*B矩阵
巧妙利用寄存器使cache的未命中次数从1.25降低到0.5

1.25
for(i = 0; i < n; i++)
{
	for(j = 0; j < n; j++ )
	{
		//对C数组进行循环
		sum = 0;
		for(k = 0; k < n; k++){
			sum += A[i][k]*B[k][j];
		}
		c[i][j] += sum;
	}
}

未命中总次数0.5
for(k = 0; k < n; k++){
	for(i = 0; i < n; i++)
	{
		r = A[i][k];
		for(j = 0; j < n; j++ )
		{
			c[i][j] += r * B[k][j];
		}
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值