深入理解计算机系统之--------优化程序性能篇

最近研读被广大计算机爱好者称之为圣经 的《深入理解计算机系统》一书,读到第五章也就是-----优化程序性能时,甚是感觉作者在计算机方面的造诣之高,简直软硬兼通,非常佩服,作者在这一张给我们提了一些编程方面的建议,如何写出高效可靠的代码,告诉我们在一个大型项目中,一个小小的 改动就可以让程序的效率有很大提高,当然,现代编译器都自带优化功能,但是即使再好的编译器 也受到妨碍优化的因素的阻碍 ,妨碍优化因素中就是程序行为中严重依赖于执行环境的方面,编译器的优化性能有高有低,比如在linux系统中,“-og"是很基本的优化,我们还可以调用”-o2"甚至“-o3”来让编译器进行更高层次的优化,这样做不用说已经最大化进行程序的优化了,但是不用说程序的规模也相对来说变得大而复杂,也可能使标准的调试工具更难得对程序进行调试,另外编译器也很小心的对程序做安全的优化,也就是说对程序运行可能遇到的所有情况在c语言标准它提供的保证下,优化后的版本和未优化的版本应该具有相同的行为,所以说限制了编译器只能进行安全优化,那么我们能否写出有利于编译器进行优化的代码来尽可能的帮助编译器进行最好的优化呢?这就是作者要表达的,作者告诉我们一些写代码的规则,尽可能让我们遵守规范,让编译器在安全的范围内进行最大化的优化。以帮助编译器,下面就让我们来见证奇迹吧!

我们写程序的目的是尽可能在所有的情况下都正确的工作,运行很快但是给出错误结果的程序毫无意义一,甚至是产生垃圾代码,有时候我们不得不编写简介的代码,这不仅为了我么自己可以看懂代码,也是为了在今后需要修改时,别人也可以很好的理解和维护我们的代码。另一方面要求,我们编写的代码还要效率高那么如何编写高效的程序呢?第一:我们必须选择一组合适的算法和数据结构,第二:我们必须编写出编译器能够有效优化以转换成高效可执行的源代码。对于第二点,理解编译器的能力和局限性很重要编写程序方式上看上去只是一点很小的改动,都会引起编译器优化方式很大的变化。第三点,针对处理运算量特别大的计算,将一个任务分成多个部分执行这些可以在多核和多处理器的某种组合上并行计算,对于第三点,我们暂且先放,后面继续讨论。

作为一个程序实例我们将用一个数据结构的运行实例,向量由两个内存块表示:头部和数据数组,头部声明如下:

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

data_t是我们用typedef声明的类型。

作为一个实例,考虑一下代码,使用某种运算将一个向量中的元素进行合并,通过使用编译时常数IDENT和OP的不同定义,这段代码可以重编译成对数据执行不同的运算;

#define IDENT 0
#define OP *

对向量元素求和使用:

#define IDENT 0
#define OP +

合并运算初步实现使用基本元素IDENT和合并运算OP的不同声明 

//implementation with maximum use of data abstraction
void combine1(vec_ptr v,data_t *dest)
{
long i;
*dest=IDENT;
for(i=0;i

这里使用的两个函数vec_length()是对向量求长度,第二个函数get_vec_element()表示返回向量中的第i个元素。

初始代码未经过任何优化,只是简单的从c语言到机器语言的翻译,通常效率较低,来看我们第一版本的优化:

1,消除循环的低效率

//implementation with maximum use of data abstraction
void combine2(vec_ptr v,data_t *dest)
{
long i;
*dest=IDENT;
long length=vec_length(v);
for(i=0;i

这个优化是常见的一种代码的移动,这类优化包括识别要执行多次但是计算结果不变的计算,因而可以把计算移动到前面单独计算其值。但是有时候编译器不会做出代码的移动这一行为应为编译器不清楚哪个函数是否有副作用,所以有时我们必须帮助编译器显示的移动代码。

2,减少过程的调用(看下例代码)

data *get_vec_start(vec_ptr v)
{
return v->data;
}
//direct access to vector data

void combine3(vec_ptr v,data_t *dest)
{
long i;
*dest=IDENT;
long length=vec_length(v);
data_t *data=get_vec_start(v);
for(i=0;i

但是奇怪的时我们做的改动并没有让程序性能有很大提升,显然,内循环中的其他操作形成瓶颈,在这里本来要借助汇编代码分析,但由于篇幅有限,在这里就直接告诉大家cause吧,

其实是指针dest在作怪,指针dest的地址存放在寄存器%rbx中他还改变了代码将第i个数据元素的指针保存在寄存器%rdx中,每次的迭代,指针都前进,累计变量的值都要从内存读入再写入内存这样就降低了效率,要解决问题,我们用一个临时变量来存放结果,然后把结果存放在dest中就行了(消除不必要的内存引用)。


data *get_vec_start(vec_ptr v)
{
return v->data;
}
//direct access to vector data

void combine3(vec_ptr v,data_t *dest)
{
long i;
*dest=IDENT;
long length=vec_length(v);
data_t *data=get_vec_start(v);
data_t acc=IDENT;
for(i=0;i

哈哈,如何这样效率就明显提高了好多。

当然,以上评价程序的性能是用程序性能的表示:每元素的周期数(CPE)来表示的,

以上总结的都是一些编码原则,低级的优化;总结来说就是以下几点

1, 消除循环的低效率,代码移动(code motion)

2,减少过程调用

3,消除不必要的存储器引用

加一点,有时候也可以用内联函数代替函数调用

要了解更高级的程序优化,还要理解现代处理器指令流水的设计,

实际的处理器中,是同时执行多条指令的(因为一条指令有多个阶段),即指令级并行,是乱序的。

      一个现代处理器框图,主要包括,指令控制单元和执行单元。

      延迟和发射(吞吐量)影响程序性能。低级优化,主要是做到见笑延迟界限,是程序性能只受发射界限的影响。

      如何分析程序性能?——数据流图,关键路径

      循环展开:设k表示循环展开的步数,k>2时,内部展开最好也用循环。循环展开的好处:减少循环索引计算和条件分支;进一步变化代码,减少关键路径上的操作数量。

      但是,循环展开后,编译器只能对整型计算做自动优化,来减少关键路径上的计算,(现代gcc能自动进行循环展开,要加编译参数 "-funroll-loops");浮点数计算,需要进行进一步优化,多个累加变量和重新结合变换。

1,上述一些低级优化,虽然有效,但是也有一些限制因素,如,寄存器一处,cpu寄存器数量有限,如果并行度p超过了寄存器数量,编译器就会把某些值放到栈中,引起性能下降;分支预测引起的性能下降。

2,存储器引发的程序性能。分为:加载和存储两方面。加载会直接影响程序性能 ; 存储不会直接影响,如果加载操作受存储影响,存储才会影响程序性能。

3,以上只是小型程序的分析方法,如何在大型程序中优化程序性能呢?需要进行程序剖析。unix系统提供了一个剖析工具GPROF。

      使用GPROF的方法:第一步,使用gcc时,加上运行时标志“-pg”  ; eg,gcc -O1 -pg prog.c -o prog

                                            第二步,执行那个程序prog,后面加参数file.txt ; eg,./prog file.txt

                                            第三步,调用GPROF分析程序产生文件gmon.out的数据 ; eg,gprof prog    ;会显示几个表格。

      分析原则:Amdahl定律。一个公式。。。其主要思想是,当我们加快系统一个部分的速度时,对系统整体性能的影响依赖于这个部分有多重要和速度提高了多少。所以,一个部分越重要(可以通过分析这个部分所用时间占从时间的百分比来看),速度提高的越多,系统总的效率提高的越多。

好了,今天就到这里吧,还有好多不足,请大家多多指教,谢谢大神!

 

你可能感兴趣的:(cs深入理解计算机系统,计算机基础)