最近在研读“深入计算机操作”一书,感受到自己的知识面也只是冰山一角。记录下自己的学习历程,也与大家一起交流。入门不久,多多指正!
这里我们使用一个数组中元素累加的一个场景,来进行低级到高阶的一步步优化;其中包括减少过程调用、降低内存别名引用和循环展开等一系列优化手段;
在单核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 10−6秒
版本迭代 | 时钟滴答数量 |
---|---|
版本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;
}
总结:
- 优化方向->减少循环体内的过程调用,减少循环次数,提高并行化
- 经过一系列的优化骚操作,我们将向量累加的程序执行效率优化了5倍左右!
提升:进一步了解汇编代码