优化程序性能之代码优化

最近在研读“深入计算机操作”一书,感受到自己的知识面也只是冰山一角。记录下自己的学习历程,也与大家一起交流。入门不久,多多指正!

这里我们使用一个数组中元素累加的一个场景,来进行低级到高阶的一步步优化;其中包括减少过程调用、降低内存别名引用和循环展开等一系列优化手段;

在单核1G内存的机器上,速度提升有接近2倍的效果!做一个不断进阶的程序猿,fighting!

定义基本数据结构

typedef long data_t;
/*Create abstract data type for vector*/
typedef struct
{
    
    
	long len;
	data_t *data;
} vec_rec,*vec_ptr;

创建向量数组

/*Create new vector of specified length*/
vec_ptr new_vec(long len)
{
    
    
	/*Allocate header structure*/
	vec_ptr result = (vec_ptr) malloc(sizeof(vec_rec));
	data_t *data = NULL;
	if(!result)
		return NULL; /*Couldn't allocate storage*/
	result->len = len;

	/*Allocate array*/
	if (len > 0){
    
    
		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;
}

下面我们就正式开始书写代码逻辑,并进行版本迭代

测试结果

我们这里使用的向量长度为1000000,累加程序循环执行10000次,计算出单次累加的时钟滴答数量。一次时钟滴答是1微妙= 1 0 − 6 10^-6 106

版本迭代 时钟滴答数量
版本1 5707
版本2 4946
版本3 2655
版本4 2655
版本5 1250
版本6 1566

版本1

获取向量的长度,循环获取向量中元素与dest进行累加。返回dest值

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;
	}
}

版本2 消除循环的低效率->代码移动

获取向量的长度vec_length该函数在循环的每一步都会调用,在当前应用中向量长度是不会改变的,所以可以把计算向量长度移动到循环外。

  • 如果这里vec_length 是获取链表中的某个值,类似O(n)复杂度的计算,代码移动到循环外执行效率将是一个飞跃式提升。
  • 疑问:为什么编译器不能做这样的优化?。因为这是一个不可靠的操作对于编译器来说,因为无法确定循环过程中向量的长度会不会改变!又一次明白了解底层对成为一个合格的程序猿至关重要
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;
	}
}

版本3 减少过程调用

从版本2的优化过程发现,减少循环体中函数的调用是一个重要的优化方向。同样我们把获取向量元素get_vec_element函数替换为数组的随机访问来获取速度的提升。

  • 这个优化颇具争议,因为他破坏了程序的模块化。如果向量存储元素的结构是一个链表就不能使用该优化。
    但是了解源码做实际的优化也是获取高性能结果的必要条件呐!
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++)
	{
    
    
		data_t val;
		*dest = *dest OP data[i];
	}
}

版本4 消除不必要的内存引用

这个优化确实比较硬核了。从汇编代码的角度来进行优化。本人才疏学浅,无法完全复现书中汇编代码。有了解的大佬还请赐教!贴上书中代码对比
在这里插入图片描述
总体意思是:由于acc(累积器)局部变量的引入消除了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 acc = IDENT;
	for (i = 0; i < length; i++)
	{
    
    
		acc = acc OP data[i];
	}
	*dest = acc;
}

版本5 循环展开

通过增加每次迭代的元素数量,减少循环的迭代次数就是循环展开。以下是展开因子为2的代码,概念比较好理解,直接上代码。

/* 2x1循环展开优化,展开因子k=2*/
void combine5(vec_ptr v, data_t *dest)
{
    
    
	long i;
	long length = vec_length(v);
	long limit = length - 1;
	data_t *data = get_vec_start(v);
	data_t acc = IDENT;
	for (i = 0; i < limit; i+=2)
	{
    
    
		acc = (acc OP data[i]) OP data[i+1];
	}

	/*Finish any remaining elements*/
	for(; i < length; i++){
    
    
		acc = acc OP data[i];
	}
	*dest = acc;
}

版本6 提高并行性

多个累积变量,一组合并运算拆分多个部分,最后合并结果来提高性能。

/* 2x2循环展开+提高并行性多个累积变量,展开因子k=2*/
void combine6(vec_ptr v, data_t *dest)
{
    
    
	long i;
	long length = vec_length(v);
	long limit = length - 1;
	data_t *data = get_vec_start(v);
	data_t acc0 = IDENT;
	data_t acc1 = IDENT;
	for (i = 0; i < limit; i+=2)
	{
    
    
		acc0 = acc0 OP data[i];
		acc1 = acc1 OP data[i+1];
	}

	/*Finish any remaining elements*/
	for(; i < length; i++){
    
    
		acc0 = acc0 OP data[i];
	}
	*dest = acc0 OP acc1;
}

github地址

总结:

  • 优化方向->减少循环体内的过程调用,减少循环次数,提高并行化
  • 经过一系列的优化骚操作,我们将向量累加的程序执行效率优化了5倍左右!

提升:进一步了解汇编代码

猜你喜欢

转载自blog.csdn.net/weixin_42662358/article/details/109397945