优化程序性能的几个方法(来自于《深刻理解计算机系统》)

  这部分的代码出自《深刻理解计算机系统》(CS:APP)第五章,其目的是经过手工改变代码结构,而不是算法效率和数据结构优化,提升执行效率。有些编译器在某些优化选项下可能会作出相似的改动。算法

  为了便于之后的查阅和使用,本文进行了摘录和简要分析,其中包含了一些我的理解。对于更深层次的原理如汇编、处理器结构等请参考原书。编程

  大体地,越靠后的代码性能越好,版本6和7性能近似,版本6略好一些。两者均能达到版本1性能的10倍左右。数据结构

  示例演示对于一个向量的全部元素完成一个运算。这个运算能够是全部元素的累加ide

#define IDENT 0
#define OP +

  或这全部元素的累计乘积 函数

#define IDENT 1
#define OP *

 

  data_t表明一种数据类型,在这个示例中能够是int、float、doubleoop

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

   对于vec_rec,有如下操做性能

建立向量学习

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(len, sizeof(data_t));
        if (!data) {
            free((void *) result);
            return NULL; /* Couldn’t allocate storage */
        }
        result->data = data;
    }
    else
        result->data = NULL;
    return result;
}
vec_ptr new_vec(long int len)

 

根据索引号获取向量元素优化

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;
}
int get_vec_element(vec_ptr v, long int index, data_t *dest)

 

得到向量长度spa

long int vec_length(vec_ptr v)
{
    return v->len;
}
long int vec_length(vec_ptr v)

 

用红色标记各个版本的改变。

 


版本1:初始版本

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

  若是不考虑错误的参数传递,这是一个可用版本,有着巨大的优化空间。

 


 

版本2:消除循环的低效率

  并不是不使用循环,而是将循环中每次都须要从新计算但实际并不会发生改变的量用常量代替。本例中,这个量是向量v的长度。相似的还有使用strlen(s)求得的字符串长度。

  显然,若是这个量在每次循环时都会被改变,是不适用的。

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

 


 

版本3:减小过程调用

  也即减小函数调用。经过对汇编代码的学习,能够知道函数调用是须要一些额外的开销的,包括建栈、传参,以及返回。经过把访问元素的函数直接扩展到代码中能够避免这个开销。可是这样作牺牲了代码的模块性和抽象性,对这种改变编写文档是一种折衷的补救措施。

  在这里,是经过把get_vec_element()展开来达到这个目的的,这须要编程人员对这个数据结构的细节有所了解,增长了阅读和改进代码的难度。

  更通用地方法是使用内联函数inline,能够兼顾模块性和抽象性。

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

 


 

版本4:消除没必要要的存储器引用

  在对于vec_rec结构操做时,其中间结果*dest是存放在存储器中的,每次取取值和更新都须要对存储器进行load或store, 要慢于对寄存器的操做。若是使用寄存器来保存中间结果,能够减小这个开销。

  这个中间结果,能够经过register显式声明为寄存器变量。不过下面的代码通过汇编后能够发现acc是存放在寄存器中的,没必要显示地声明。

  然而,中间变量并不是越多越好。原书5.11.1节展现了一个情形,若是同时使用的中间变量数过多,会出现“寄存器溢出”现象,部分中间变量仍然须要经过存储器保存。同理,多于机器支持能力的register声明的变量并不必定所有使用了寄存器来保存。

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

 


版本5:循环展开

  这个优化措施利用了对CPU数据流的知识,比汇编代码更接近机器底层。简单地说是利用了CPU的并行性,将数据分红不相关的部分并行地处理。版本5~7的更多细节和原理能够参考原书。相似的原理在练习题5.5和5.6中展现了为何Horner法比通常的多项式求值的运算次数少,反而更慢的缘由。

  展开的次数能够根据状况而定,下面的代码只展开了两次。对于未处理的部分元素,不能遗漏。

  gcc能够经过-funroll-loops选项执行循环展开。

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

 


 

版本6与版本7:提升并行性

  和版本5的思想相似,但因为并行化更高,性能更好一些,充分利用了向量中各个元素的不相关性。

  版本6使用多个累积变量方法。

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

 

  版本7是在版本5的基础上打破顺序相关,改变了并行执行的操做数量。

void combine7(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]);
    //in combine5:
    //acc =
( acc OP data[i]) OP data[i+1];
    }

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

 


补充说明

  这个示例中没有提到的改进方法还有:书写适合条件传送实现的代码,下面是原书的两段用于对比的代码,后者更适合条件传送实现。

void minmax1(int a[], int b[], int n) {
    int i;
    for(i=0;i<n; i++) {
        if (a[i] > b[i]) {
            int t = a[i];
            a[i] = b[i];
            b[i] = t;
        }
    }
}

/* Rearrange two vectors so that for each i, b[i] >= a[i] */
void minmax2(int a[], int b[], int n) {
    int i;
    for(i=0;i<n; i++) {
        int min = a[i] < b[i] ? a[i] : b[i];
        int max = a[i] < b[i] ? b[i] : a[i];
        a[i] = min;
        b[i] = max;
    }
}
相关文章
相关标签/搜索