程序优化方法——CSAPP 读书笔记

原创 2016年02月25日 13:01:33

Optimizing Program Performance

刚刚把CSAPP第五章看完,感触还是很深的。看完这章之后才知道以前以为自己会的好多东西其实都是会的表象,深层次的东西还是需要慢慢的发掘吸收的啊!
这一章主要讲的程序优化方面的东西。从底层,(汇编层,CPU模型层)对一个程序进行分析,找出其瓶颈,并针对性的对其进行优化。最终达到一个性能的提升。废话少说,直接上代码:
Implementation of vector abstract data type

typedef int data_t;

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

#ifdef COMBIN_SUM
    #define IDENT 0
    #define OP +
#else
    #define IDENT 1;
    #define OP *;
#endif

/*Create vector of specified length*/
vec_ptr new_vec(long int len)
{
    /*Allocate header structure*/
    vec_ptr result = (vec_ptr)malloc(sizeof(vec_rec));
    if(!result)
        return NULL; /*Couldn;t allocate storage*/
    result->len = len;
    /*Allocate array*/
    if(len > 0) {
        data_t *data = (data_t*)calloc(sizeof(data_t)*len);
        if(!data){
            free((void*)result);
            return NULL;
        }
        result->data = data;
    }else
        result->data = NULL;
    return result;
}

/*
 * Retrieve vector element and store at dest.
 * Return 0 (out of bounds) or 1 (successful)
*/
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;
}

/* Return length of vector */
long int vec_length(vec_ptr v)
{
    return v->len;
}

上面是一个抽象数据类型 Vector的实现,也是后面例程的基础。
例程的主要目的是实现一个向量内所有元素的相加或者相乘,并返回结果。

/*Implementation with maximum use of data abstraction*/
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;
    }
}

上面的代码是第一个版本,也是没有经过任何优化的版本。记为combine1其性能为(单位:CPE cycles per elements):
combine1 性能指标

下面我们将会按照一定的原则逐步的对这段代码进行优化,从而在保证精度的同时提升函数的执行效率。

Eliminating Loop Inefficiencies

翻译过来就是消除因为循环而带来的效率低下的情况,比如那种重复的无意义的循环调用等。

我们可以看到在 combine1 中,

for(i=0; i

/*Move call to vec_length out of loop*/
void combine2(vec_ptr v, data_t *dest)
{
    long int i;
    long int 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;
    }
}

按照惯例,其性能指标为:

combine2 性能指标

从上表的比较中我们可以发现,在某些特定的数据类型下(int),这种改写有着显著的性能提升,而在有些数据类型中,则收效甚微。这又涉及到优化中的一个重要的原则:需要评估一个函数的性能瓶颈,并针对性的进行优化。这一块会在以后的部分慢慢讨论。

Reducing Procedure Calls

减少函数的调用,函数调用会导致性能的低下,从而成为程序优化的一个重要模块。在combine2中,在每一次迭代中,我们都需要调用get_vec_element,去遍历向量中的元素。这个函数需要检查传进来的参数i是否越界,这是一个明显的多余的操作,因为在外层循环中就已经保证了这个i是不会超过向量的长度的,同时函数调用本身还需要消耗相应的资源。

换一种写法,假设我们给抽象数据类型vec_rec增加一个函数 get_vec_start,返回数据数组的起始地址。那么我们就可以对combine2进行改写,得到combine3:

data_t *get_vec_start(vec_ptr v)
{
    return v->data;
}

/*Direct access to vector data*/
void combine3(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    data_t *data = get_vec_start(v);

    *dest = IDENT;
    for(i=0; i<length; i++){
        *dest = *dest OP data[i];
    }
}

按照惯例,其性能指标为:
combine3 性能指标

从结果上来看,这种写法的提升更加让人迷茫,除了整数的加法外,剩余的情况几乎没有太大的变化。产生这种结果的原因和cpu运行时的一个叫 branch predict的机制有关。有机会的话,会在后面的部分写出来。

Eliminating Unneeded Menory References

减少不必要的内存引用/解引用,combine3的代码中,在循环的过程中,累积计算的中间值到dest指针中,以下这段由编译器生成的汇编代码(X86_64),可以明确的看出这个问题。

combine3: data_t= float,OP = *
i in %rdx, data in %rax,  dest in %rbp
.L498:                          Loop:
    movss (%rbp), %xmm0         Read product from dest
    mulss (%rax,%rdx,4), %xmm0  Multiply product by data[i]
    movss %xmmo, (%rbp)         Store product at dest
    addq  $1, %rdx             Increment i
    cmpq  %rdx, %r12            Compare i: limit
    jg    .L498                 if > goto Loop 

从上述的汇编代码中我们可以看到,在第i次迭代的过程中,程序读取位于dest位置的的数据,拿他乘以data[i],然后把乘积存回dest。这里的读写操作就是多余的,因为在每次迭代中,读取进来的值和上一次迭代结束后存回去的值是同一个值。所以我们可以拿掉这个不必要的读写操作,得到combine4

/*Accumulate result in local variable*/
void combine4(vec_ptr v, data_t *dest)
{
    long int i;
    lont 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;
}

combine3相比,在循环的每一次迭代中,我们把内存操作从两个读一个写操作降到了一个读操作:

combine4: data_t = float, OP = *

i in %rdx, data in %rax, limit in %rbp, acc in %xmm0

.L488:                          Loop:
    mulss (%rax,%rdx,4), %xmm0  Mutilpy acc by data[i]
    addq  $1, %rdx             Increment i
    cmpq  %rdx, %rbp            Compare limit :i
    jg    .L48                  if > , goto loop

通过这么简单的一个改写,性能提升效果如下:
combine4 性能指标

从上面四个代码的改写过程,我们可以看到一个显著的性能提升,而这些改写都是没有依赖任何目标机器的特性。换句话说就是这写原则是通用的。
下面针对现代的cpu特性,我们还可以进一步做一些优化操作:

Loop Unrolling

循环展开,循环展开可以从如下两个方面提升程序的性能,其一是减少了对程序结果没有贡献的额外的操作,比如循环变量和条件分支。 其二是他给我们提供了一个更广泛的优化空间,从而可以减少在主路径上的操作,提升性能。从而我们得到了combine5

/*Unroll loop by 2*/
void combine5(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    long int limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc = IDENT;

    /*Combine 2 elements at a time*/
    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;
}

其性能提升为:
combine5 性能指标

Enhancing Parallelism

增加并行,combine5提升了 int 型的性能, 然而却于float,double 无补。问题出在哪里呢?
请看下面的一版改写combine6

/*Unroll loop by 2, 2-way parallelism */
void combine6(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    long int limit = length - 1;
    data_t *data = get_vec_start(v);
    data_t acc0 = IDENT;
    data_t acc1 = IDENT;

    /*Combine 2 elements at a time*/
    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;
}

通过一个上述一个简单的操作,我们可以获取以下的性能提示:

到这一步其实关于这个函数的优化问题就基本上可以告一段落了,读书笔记也到此结束了。接下来要完成的就是针对这从头到尾的这些个优化做一些分析了。比如性能测试表中的数值是怎么得到的,比如为什么有的操作只对int提升效果较大,对float和double却几乎没有什么提升。比如combine6的写法有没有什么问题。会不会导致程序的结果不一样等…… 问题是无尽的。要想彻底回答以上种种疑问,就需要对编译器的优化准则,对现代CPU模型有一个深入的了解。嗯,我会重读这一章,再写一个全局意义上的感想与大家分享。未完待续

Reducing Procedure Calls

data_t* get_vec_start(vec_ptr v)  {      return v->data;  }    void combine3(vec_ptr v, data_t* dest...
  • hezhch123
  • hezhch123
  • 2012年03月08日 20:14
  • 326

CSAPP第四章读书笔记

(1)将指令编码成为字节序列(1)将指令编码成为字节序列每一个不同类型的指令都有着不一样的起始字节,根据类型的不同编码的长度和格式也不一样。 注意立即数和地址在小端序列的存储中要倒着排序。(2)我们...
  • pp634077956
  • pp634077956
  • 2016年10月18日 10:10
  • 548

CSAPP第八章-异常控制流(三)信号

每种信号类型都对应某种系统事件。底层的硬件异常是由内核异常处理程序处理的,正常情况下,堆用户进程而言是不可见的。信号提供了一种机制,统治用户进程发生了这些异常。当一个子进程终止或者停止时,内核会发送一...
  • WMLWONDER
  • WMLWONDER
  • 2016年12月18日 17:21
  • 268

Csapp读书笔记:第五章

(1)(1)安全优化,编译器在进行优化的时候必须考虑所有的情况,只执行安全的优化。所以可能会限制很多可能的优化策略。如果写出上面这样的代码,当xp==yp的时候,最后的结果等于0。否则xp和yp指向的...
  • pp634077956
  • pp634077956
  • 2016年10月25日 14:09
  • 2105

【U3D】后期渲染性能优化之---减少Draw Calls的调用

首先我们来谈谈Standard Shader(标准着色器),Standard Shader是一种基于物理渲染的着色器,它可以支持各种光照条件下的渲染效果,并支持跨平台调用,它的Texture slot...
  • wdmzjzlym
  • wdmzjzlym
  • 2016年05月07日 18:49
  • 2790

CSAPP:优化程序性能(一)

编写高效程序需要做到以下几点: 第一,必须选择一组适当的算法和数据结构 第二,必须编写出编译器能够有效优化以转换高效可执行代码的源代码(理解优化编译器的能力和局限性很重要) 程序员必须在实现和维护程序...
  • a547720714
  • a547720714
  • 2017年06月20日 13:39
  • 286

Oracle PLSQL Procedure 如何进行性能调优分析

在Java的性能调优分析中,可以使用 JProfiler 分析JVM运行时的CPU消耗、Memory占用、Thread情况等信息。对于Java代码中调用的Oracle的存储过程、函数它也能输出调用时间...
  • u012284514
  • u012284514
  • 2015年10月22日 11:24
  • 998

CSAPP:优化程序性能(四)

了解一些限制程序性能的因素 一. 寄存器溢出 如果我们的并行度P超过了可用寄存器的数量,那么编译器就会通知溢出,将某些临时值存放在内存中,通常是运行时堆栈上分配空间,聚个例子,当把combine6的多...
  • a547720714
  • a547720714
  • 2017年06月21日 22:00
  • 189

读书笔记:CSAPP

Chapter1基本思想信息 是一系列的 位(bit) 和这些数据的 上下文(context) 。上下文决定了如何解码这些信息。例如,一串0和1的字符串,根据上下文可以解码为 整型 、浮点数 或 AS...
  • Mukae1997
  • Mukae1997
  • 2017年03月15日 20:57
  • 189

CSAPP:优化程序性能(二)

程序示例 为了说明一个抽象程序是如何被系统地转换成更有效的代码的,我们使用基于如下所示的向量数据结构的运行示例 向量由两个内存块表示,头部和数据数组,头部声明结构如下, data_t代表基本数据类...
  • a547720714
  • a547720714
  • 2017年06月20日 16:02
  • 141
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:程序优化方法——CSAPP 读书笔记
举报原因:
原因补充:

(最多只允许输入30个字)