深入理解计算机系统(csapp)阅读笔记——第五章优化程序性能

1.优化编译器的能力和局限性

  • 两种指针可能指向同一个内存位置的情况称为内存别名引用,在只执行安全的优化中,编译器必须假设不同的指针可能会指向内存的同一个位置。这样就限制了编译器的优化,如下面例子:
//原来的代码
void twiddle1(long *xp,long *yp)
{
	*xp +=*yp;
	*xp +=*yp;	
}
//优化后的代码
void twiddle1(long *xp,long *yp)
{
	*xp += 2*(*yp);
}

如果yp和xp指向同一个内存,原来的代码得到4倍结果,而优化后的代码却是3倍,故编译器这样优化会出问题

  • 函数调用产生的问题:
int counter = 0;
long f()
{
	return counter++;
}
//原始代码
long func1()
{
	return f()+f()+f()+f();
}
//优化后的代码
long func2()
{
	return 4*f();
}

由上图可见,虽然优化后只调用了一次f(),但是优化后得到的结果并不一样
解决办法:我们可以将f()声明为inline内敛函数 ,即调用的时候直接内部展开,这样既可以减少了函数调用的开销,也允许对展开的代码做进一步优化,适合一些经常被调用的少量代码

2.表示程序性能

  • 度量标准:每元素的周期数(Cycles Per Elment,CPE),作为一种表示程序性能并指导我们改进代码的方法。
  • 处理器活动的顺序是由时钟控制的,时钟提供了某个频率的规律信号,通常用千兆赫兹(GHz)表示,例如,当表明一个系统有4GHz处理器,表示处理器时钟运行频率为每秒4*109个周期。

3.用于以下优化的原始函数

typedef long data_t;
typedef IDENT 1 //或0
#define OP *    //或+

typedef struct{
	long len;
	data_t *data;
}vec_rec,*vec_ptr;

vec_ptr new_vec(long len)
{
	vec_ptr result = (vec_ptr)malloc(sizeof(vec_rec));
	data_t *data = NULL;
	if(!result)  //如果分配失败
		return NULL;
	if(len>0){
		//动态分配数组calloc,指定data_t大小的len个空间
		data = (data_t*)calloc(len,sizeof(data_t));
		if(!data){
			free((void *)result);
			return NULL;
		}
	}
	result->data = data;
	return result;
}

int get_vec_element(vec_ptr v,long index,data_t *dest)
{
	if(index<0 || index>=v-len)
		return 0;
	*dest = v->data[index];
	return 1;
}

long vec_length(vec_ptr v)
{
	return v->len;
}

void combine1(vec_ptr v,data_t *dest)
{
	long i;
	*dest = IDENT;
	for(i = 0;i<vec_length(v);i++){
		data_t val;
		get_vec_element(v,i,&val);
		*dest = *dest OP val;
	}
}

4.优化1——消除循环的低效率

  • 将循环中要执行多次但是计算结果不会改变的计算提出来
void combine2(vec_ptr v,data_t *dest)
{
	long i;
	long length = vec_length(v);
	*dest = IDENT;
	for(i = 0;i<length;i++){
		data_t val;
		get_vec_element(v,i,&val);
		*dest = *dest OP val;
	}
}

5.优化2——减少过程调用

  • 减少循环中过程的调用,如get_vec_element每次都会进行边界检查,但是我们这里并不需要边界检查,因为所有索引都是合法的,所以我们直接获取首地址后使用data[i],缺点是破坏了程序的模块性,我们并不应该知道他的元素到底是以数组存储还是链表之类的存储。而且此时的效率并没有显著提升
data_t* get_vec_start(vec_ptr v)
{
	return v->data;
}
void combine3(vec_ptr v,data_t *dest)
{
	long i;
	long length = vec_length(v);
	data_t *data = get_vec_start(v);
	*dest = IDENT;
	for(i = 0;i<length;i++){
		*dest = *dest OP data[i];
	}
}

6.优化3——消除不必要的内存引用

  • 在循环中消除不必要的内存引用,像上述题目中的dest的读写过于频繁,但是不是必要的,每次读的都是上次写的,所以使用临时变量计算值就行
void combine4(vec_ptr v,data_t *dest)
{
	long i;
	long length = vec_length(v);
	data_t *data = get_vec_start(v);
	data_t sum = IDENT;
	for(i = 0;i<length;i++){
		sum = sum OP data[i];
	}
    *dest = sum;
}

7.两个界限

  • 指令级并行:在代码级上,看上去似乎是一次执行一条指令,每条指令都包括从寄存器或内存取值,执行一个操作,并把结果存回到一个寄存器或内存位置。在实际的处理器中,是同时对多条指令求值的。
  • 描述程序性能的两个下界:
    • 延迟界限:一系列操作必须按照严格顺序执行,因为在下一条指令开始之前,这条指令必须结束。主要是代码中的数据相关限制了处理器利用指令级并行的能力
    • 吞吐量界限:刻画了处理器功能单元的原始计算能力。这个界限是程序性能的中继限制。

8.循环展开

  • **循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。**循环展开能够从两个方面改进程序的性能。
    • 它减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支
    • 提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量
  • 优化代码:
void combine5(vec_ptr v,data_t *dest)
{
	long i;
	long length = vec_length(v);
	data_t *data = get_vec_start(v);
	data_t sum = IDENT;
	for(i = 0;i<length-1;i+=2){
		sum = sum OP data[i] OP data[i+1];
	}
	//完成剩下的索引
	for(;i<length;i++){
		sum = sum OP data[i];
	}
    *dest = sum;
}

9.提高并行性

  • 执行加法和乘法的功能单元是完全流水化的。所以对于一个可结合和可交换的合并运算来说,比如说整数加法或乘法,我们可以通过将一组合并运算分割成两个或更多的部分,并在最后合并结果来提高性能
void combine6(vec_ptr v,data_t *dest)
{
	long i;
	long length = vec_length(v);
	data_t *data = get_vec_start(v);
	data_t sum1 = IDENT;
	data_t sum2 = IDENT;
	for(i = 0;i<length-1;i+=2){
		//两个运算几乎并行运行,流水线
		sum1 = sum1 OP data[i] 
		sum2 = sum2 OP data[i+1];
	}
	//完成剩下的索引
	for(;i<length;i++){
		sum1 = sum1 OP data[i];
	}
    *dest = sum1 OP sum2;
}

或者:

void combine7(vec_ptr v,data_t *dest)
{
	long i;
	long length = vec_length(v);
	data_t *data = get_vec_start(v);
	data_t sum = IDENT;
	for(i = 0;i<length-1;i+=2){
		sum = sum OP (data[i] OP data[i+1]);
	}
	//完成剩下的索引
	for(;i<length;i++){
		sum = sum OP data[i];
	}
    *dest = sum;
}

10.一些限制因素

(1)寄存器溢出

  • 循环并行性的好处受汇编代码描述计算的能力限制。如果我们的并行度p超过了可用的寄存器数量,那么编译器会诉诸溢出。

(2)分支预测和预测错误处罚

  • 当分支预测逻辑不能正确一个分支是否要跳转的时候,条件分支可会招致很大的预测错误处罚
  • 要求程序员尽量写出适合用条件传送语句的程序

10.理解内存性能

  • 所有的现代处理器都包含一个或多个高速缓存存储器,以对这样少量的存储器提供快速的访问
发布了33 篇原创文章 · 获赞 3 · 访问量 615

猜你喜欢

转载自blog.csdn.net/qq_43647628/article/details/104615217