1. 程式人生 > >CSAPP:優化程式效能(一)

CSAPP:優化程式效能(一)

編寫高效程式需要做到以下幾點:

第一,必須選擇一組適當的演算法和資料結構

第二,必須編寫出編譯器能夠有效優化以轉換高效可執行程式碼的原始碼(理解優化編譯器的能力和侷限性很重要)

程式設計師必須在實現和維護程式的簡單性和運算速度之間做出權衡,幾分鐘就能編寫一個簡單的插入程式,而一個高效的排序演算法程式可能需要一天或更長時間來實現和優化,

大多數編譯器,例如GCC向用戶提供了一些對它們所使用的優化的限制,最簡單的控制就是指定優化級別,以命令列選項-Og 使用一組基本的優化,或者-O1或者更高

(-O2或-O3),編譯器必須很小心地對程式進行安全的優化,消除造成不希望的執行時行為的一些可能的原因,為了理解決定一種程式轉換是否安全的難度,看看如下兩個過程

記憶體別名使用

void twiddle1(long *xp, long *yp)
{
    *xp += *yp;
    *xp += *yp;
}

void twiddle2(long *xp, long *yp)
{
    *xp += 2 * *yp;
}

兩個過程似乎有相同的行為,函式twiddle2的效率更高一些,因為它只需要3次記憶體引用,而twiddle1需要6次記憶體引用,不過考慮*xp等於*yp的情況,twiddle1的結果是*xp增加了4倍,而twiddle2的結果是*xp的值增加了3倍,編譯器不知道函式會被如何呼叫,因為必須假設引數xp和yp必須相等,因此不可能產生twiddle2作為twiddle1的優化版本(兩個指標指向記憶體同一位置的情況稱為“記憶體別名使用”)。
x = 1000;
y = 3000;
*q = y;
*p = x;
t1 = *q;
t1的值取決於p和q是否指向記憶體的同一位置,如果不是則t1等於3000,如果是的話則t1等於1000。這造成了一個主要的妨礙優化因素,嚴重限制了編譯器優化策略。

函式呼叫

long f();

long func1(){
    return f() + f() + f() +f();
}

long func2(){
    return 4 *f();
}

最初看上去兩個過程結果相同,但是考慮下面f程式碼
long counter = 0;

long f(){
    return counter++;
}

函式有副作用——它修改了全域性變數的一部分,改變呼叫次數會改變程式的行為,大多數編譯器都不會考慮程式是否具有副作用,它會假設最糟的情況,並保持所有的函式呼叫不變。

行內函數替換

包含函式呼叫的程式碼可以使用行內函數替換來進行優化,將函式呼叫替換為函式體

將func1替換如下:

long func1in(){
    long t = counter++;
    t += counter++;
    t += counter++;
    t += counter++;
    return t;
}
這樣既減少了函式呼叫開銷也可以對程式碼進一步優化:
long func1opt(){
    long t = 4 * counter +6;
    counter += 4;
    return t;
}
GCC的最近版本會嘗試這種優化,並且只嘗試在單個檔案中定義函式的內聯,這意味著它無法應用於常見的情況——檔案之間的函式呼叫。

就優化能力來說GCC是勝任的,但是它不會做那種激進變換的優化。

表示程式效能

引入度量標準每元素的週期數(Cycles Per Element,CPE)來表示程式效能並指導改進程式碼

處理器活動順序由時鐘控制,時鐘提供某個頻率的規律訊號,通常用GHz(千兆赫茲)即十億週期每秒來表示,例如一個處理器是4GHz,這表示處理器時鐘的執行頻率為每秒4e9個週期,這裡要強調一點:

在中國,1兆 = 1e12 也就是1萬億

在西方,1兆 = 1e6 也就是1百萬 所以千兆就是1e9

每個週期的時間就是時鐘頻率的倒數,也就是1e-9秒(1納秒)

計算長度為n的向量的前置和,對於向量a=<a0, a1 , ... , an-1>,前置和向量p=<p0, p1, ..., pn-1>定義為

p0 = a0

pi = pi-1 + ai, 1<= i < n

void psum1 (float a[], float p[], long n)
{
    long i;
    p[0] = a[0];
    for(i = 1, i < n, i++)
        p[i] = p[i-1] + a[i];
}

void psum2(float a[], float p[], long n)
{
    long i;
    p[0] = a[0];
    for( i=1; i < n-1; i +=2){
        float mid_value = p[i-1] + a[i];
        p[i] = mid_value;
        p[i+1] = mid_vlaue + a[i+1];
    }
    if (i < n)
        p[i] = p[i-1] + a[i];
}
psum1每次迭代計算結果向量的一個元素

psum2函式使用迴圈展開,每次迭代計算兩個元素

這個過程所需的時間可以使用一個常數加上一個與被處理元素個數成正比的因子來描述(應該就是二元一次方程吧),使用最小二乘擬合,我們發現psum1和psum2的執行時間(以時鐘週期為單位)分別接近於368 + 9.0n 和 368 + 6.0n,程式碼計時和初始化過程、準備迴圈以及完成過程的開銷為368個週期,再加上每個元素6.0或9.0週期的線性因子,這些項中的係數成為每元素的週期數(簡稱CPE),根據這種度量標準,psum2的CPE為6.0,psum1的CPE為9.0。