優化程序性能的幾個方法(來自於《深刻理解計算機系統》)

  這部分的代碼出自《深刻理解計算機系統》(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;
    }
}
相關文章
相關標籤/搜索