本文主要是通过学习深入理解计算机系统第五章-性能优化之后的理解和总结。第五章主要目的通过对底层硬件架构和机制(汇编、处理器架构)的理解,让程序员写出更加高效(性能)的代码。
对于性能需要不是很急切的地方进行性能优化的意义不是很大,进行性能优化是需要成本的,需要对原始代码进行重构。随着计算机技术不断发展,处理器性能的提高,往往性能可以通过硬件来弥补,但是有些时候需要对在软件层面进行性能优化。比如大数据处理、多媒体处理、网络传输、数学、算法计算、嵌入式等硬件条件有限的条件下对于性能优化提出更高的要求。
性能提升意味着用户体验的提升,往往一个软件的成败就是在于这些用户体验细微的差别,例如是网络多媒体播放器,如果传输视频卡一些,也许这个软件就会被别人所抛弃。优酷的成功就在于:顺畅的播放体验及强大的资源。
提升软件性能需要各个方面的努力,硬件支持(处理器、内存)、GCC编译器、上层应用软件的开发。在同样的硬件条件下,软件如何提升?在多数的教科书中,往往是介绍了GCC的优化,但是GCC的优化是有局限的,它是以“安全”(保证原始软件功能的前提下)进行的优化,它是一种通用的优化,对于GCC自己不确定的,就默认为不优化。
软件的优化分为几个层级:
1.高级优化:数据结构、高级算法的优化。
2.基本编码原则,消除连续的函数调用,消除不必要的存储器引用。
3.低级优化,底层源码级优化,可根据具体处理器,展开循环,累计变量和重新结合,用功能的风格重写条件操作。
在这里主要是介绍低级优化的层级,不改变原始软件的数据结构及高级算法,只针对源码进行较为底层的优化。通过一些技术可以让GCC更好识别源码,编译出性能更高的源码。
CPE(Cycles Per Element),每个元素的周期数,每个时钟周期的时间是时钟频率的倒数,一般用纳秒标示。
GCC优化建议利用-O1以上的,通过对编译器选项的配置可以达到性能的优化。
优化基础程序如下,combine1是我们优化的基础程序,通过对它的优化,讲述优化的功能和效果。
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
typedef float data_t;
#define IDENT 1
#define OP *
typedef struct{
long int len;
data_t *data;
}vec_rec,*vec_ptr;
vec_ptr new_vec(long int len)
{
vec_ptr result = (vec_ptr) malloc(sizeof(vec_rec));
if(!result)
return NULL;
result->len = len;
if(len > 0){
data_t *data = (data_t *)calloc(len,sizeof(data_t));
if(!data){
free((void *)result);
return NULL;
}
result->data = data;
}
else
result->data = NULL;
return result;
}
int get_vec_element(vec_ptr v,long int index,data_t *dest)
{
if(index < 0 || index >= v->len)
return 0;
*dest = v->data[index];
return 1;
}
long int vec_length(vec_ptr v)
{
return v->len;
}
void combine1(vec_ptr v, data_t *dest)
{
long int i;
*dest = IDENT;
for(i = 0;i < vec_length(v);i++){
data_t val;
get_vec_element(v,i,&val);
*dest = *dest OP val;
}
}
int main()
{
struct timeval tpstart,tpend;
float timeuse;
vec_ptr a = new_vec(100000);
data_t *dest = (data_t*)malloc(sizeof(data_t));
printf("test the psum1 time :\n");
gettimeofday(&tpstart,NULL);
combine1(a,dest);
gettimeofday(&tpend,NULL);
timeuse = 1000000*(tpend.tv_sec-tpstart.tv_sec) + tpend.tv_usec - tpstart.tv_usec;
timeuse/=1000000;
printf("used time:%f\n",timeuse);
return 0;
}
如果采用-O1选项进行优化,效果对比为:
gcc -O1 test.c -o test
下图是书中分析数据:
如下是在我的兆芯开发版上实验结果:
test1_O0_add_int 0.003695
test1_O2_add_int 0.000283
test_O1_add_int 0.001759
这就告诉我们以后编程序的时候至少添加O1选项,这样会使程序性能得到提升。
void combine2(vec_ptr v, data_t *dest)
{
long int i;
*dest = IDENT;
long int length = vec_length(v);
for(i = 0;i < length;i++){
data_t val;
get_vec_element(v,i,&val);
*dest = *dest OP val;
}
}
将vec_length()函数拿到循环外面,性能得到提升:
本机测试结果:
test1_O0_add_int 0.003695
test2_O0_add_int 0.003246
combine2中有函数get_vec_element(),这个函数实质是提取元素值,可以转换成获取地址,然后取值的方式提取,就减少了循环内部函数调用。
void combine3(vec_ptr v, data_t *dest)
{
long int i;
*dest = IDENT;
long int length = vec_length(v);
data_t *data = get_vec_start(v);
for(i = 0;i < length;i++){
*dest = *dest OP data[i];
}
}
性能分析如图:
本机测试:
test2_O0_add_int 0.003246
test3_O0_add_int 0.001576
告诉我们尽量减少循环内部的函数调用。
循环内部尽量减少对指针的引用,可以在循环体外面替换为变量的形式。因为在循环体内部,使用引用的形式,需要从存储器中调用数据加载到寄存器,然后对寄存器数据处理,之后再把寄存器的内容存放到存储器,过程较为复杂。如果采用临时变量的形式可以提高软件的性能。
void combine4(vec_ptr v, data_t *dest)
{
long int i;
long int 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;
}
性能分析:
本机测试:
test3_O0_add_int 0.001576
test4_O0_add_int 0.001313
能够利用变量代替就别用指针,利用引用操作会造成编译器编译的汇编代码为加载存储寄存器,降低了性能。
到目前为止,我们运用的优化都不依赖于目标机器的任何特性,这些优化只是简单地降低了过程调用的开销,以及消除了一些重大的“妨碍优化的因素”,这些因素会给优化编译器造成困难。随着试图进一步提升性能,我们必须考虑利用处理器微体系结构的优化,也就是处理器用来执行指令的底层系统设计。要想获得充分提高的性能,需要自信地分析程序,同时代码的生成也要针对目标处理器进行调整。尽管如此,我们还是能够运用一些基本的优化,在很大一类处理器上产生整体的性能提高。
上图表示了处理器大概的处理过程,针对不同处理器的体系架构,优化处理的方法可能不一样。例如兆芯nano CPU和Intel的结构是不一样的,主要不同有:
1.预测分支结构不同。
2.缓存大小及结构不同。
3.多核的结构不同。
4.逻辑处理单元不同。(加法器个数不同)
5.不同指令集执行的时间不同。
这些的不同导致对于不同的处理器上,优化程序的方法不一样。
如图是Intel I7的逻辑处理功能单元的性能,包括延迟及发射时间,根据它以及源码就可以估计程序的性能。一般CPU的逻辑处理功能单元的性能会在官方网站上给出。
数据流图分析程序性能,例如combine4:
void combine4(vec_ptr v, data_t *dest)
{
long int i;
long int 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;
}
循环体是影响执行时间的主要因素,对于一个循环体内部只读、只写不产生循环相关的寄存器不对程序起主导因素,只有形成循环相关的寄存器才起到重要影响(就是从本次循环到下一次读写并且产生前后相关的操作的寄存器),如图a,b经过简化就找到了影响combine4的主要限制因素。