1. 程式人生 > >優化C語言程式碼常用的幾招

優化C語言程式碼常用的幾招

在效能優化方面永遠注意80-20原則,即20%的程式消耗了80%的執行時間,因而我們要改進效率,最主要是考慮改進那20%的程式碼。不要優化程式中開銷不大的那80%,這是勞而無功的。

第一招:以空間換時間

計算機程式中最大的矛盾是空間和時間的矛盾,那麼,從這個角度出發逆向思維來考慮程式的效率問題,我們就有了解決問題的第1招--以空間換時間。比如說字串的賦值:

方法A:通常的辦法

#define LEN 32
char string1 [LEN];
memset (string1,0,LEN);
strcpy (string1,"This is a example!!");

方法B:

const char string2[LEN] ="This is a example!";
char * cp;
cp = string2 ;

使用的時候可以直接用指標來操作。

從 上面的例子可以看出,A和B的效率是不能比的。在同樣的儲存空間下,B直接使用指標就可以操作了,而A需要呼叫兩個字元函式才能完成。B的缺點在於靈活性 沒有A好。在需要頻繁更改一個字串內容的時候,A具有更好的靈活性;如果採用方法B,則需要預存許多字串,雖然佔用了大量的記憶體,但是獲得了程式執行 的高效率。

如果系統的實時性要求很高,記憶體還有一些,那我推薦你使用該招數。

第二招: 使用巨集而不是函式。


    這也是第一招的變招。函式和巨集的區別就在於,巨集佔用了大量的空間,而函式佔用了時間。大家要知道的是,函式呼叫是要使用系統的棧來儲存資料的,如果編譯器 裡有棧檢查選 項,一般在函式的頭會嵌入一些彙編語句對當前棧進行檢查;同時,CPU也要在函式呼叫時儲存和恢復當前的現場,進行壓棧和彈棧操作,所以,函式呼叫需要一 些CPU時間。 而巨集不存在這個問題。巨集僅僅作為預先寫好的程式碼嵌入到當前程式,不會產生函式呼叫,所以僅僅是佔用了空間,在頻繁呼叫同一個巨集的時候,該現象尤其突出。

舉例如下:

方法C:

#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
int BIT_MASK(int __bf)
{
return ((1U << (bw ## __bf)) - 1)<< (bs ## __bf);
}
void SET_BITS(int __dst,
int __bf, int __val)
{
__dst = ((__dst) & ~(BIT_MASK(__bf))) |
/
(((__val) << (bs ## __bf))
& (BIT_MASK(__bf))))
}
SET_BITS(MCDR2, MCDR2_ADDRESS,ReGISterNumber);

方法D:

#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
#define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS)
#define BIT_MASK(__bf)
(((1U << (bw ## __bf)) - 1)
<< (bs ## __bf))
#define SET_BITS(__dst, __bf, __val)
/
((__dst) = ((__dst) & ~(BIT_MASK(__bf)))
| /
(((__val) << (bs ## __bf))
& (BIT_MASK(__bf))))
SET_BITS(MCDR2, MCDR2_ADDRESS,
RegisterNumber);

D方法是我看到的最好的置位操作函式,是ARM公司原始碼的一部分,在短短的三行內實現了很多功能,幾乎涵蓋了所有的位操作功能。C方法是其變體,其中滋味還需大家仔細體會。

第三招:數學方法解決問題


現在我們演繹高效C語言編寫的第二招--採用數學方法來解決問題。數學是計算機之母,沒有數學的依據和基礎,就沒有計算機的發展,所以在編寫程式的時候,採用一些數學方法會對程式的執行效率有數量級的提高。舉例如下,求 1~100的和。

方法E:

int I , j;
for (I = 1 ;I<=100; I ++)
{
j += I;
}

方法F

int I;
I = (100 * (1+100)) / 2

這 個例子是我印象最深的一個數學用例,是我的計算機啟蒙老師考我的。當時我只有小學三年級,可惜我當時不知道用公式 N×(N+1)/ 2 來解決這個問題。方法E迴圈了100次才解決問題,也就是說最少用了100個賦值,100個判斷,200個加法(I和j);而方法F僅僅用了1個加法,1 次乘法,1次除法。效果自然不言而喻。所以,現在我在程式設計序的時候,更多的是動腦筋找規律,最大限度地發揮數學的威力來提高程式執行的效率。

第四招:使用位操作

    使用位操作。減少除法和取模的運算。在計算機程式中資料的位是可以操作的最小資料單位,理論上可以用"位運算"來完成所有的運算和操作。一般的位操作是用來控制硬體的,或者做資料變換使用,但是,靈活的位操作可以有效地提高程式執行的效率。舉例如下:

方法G

int I,J;
I = 257 /8;
J = 456 % 32;

方法H

int I,J;
I = 257 >>3;
J = 456 - (456 >> 4 << 4);

在 字面上好像H比G麻煩了好多,但是,仔細檢視產生的彙編程式碼就會明白,方法G呼叫了基本的取模函式和除法函式,既有函式呼叫,還有很多彙編程式碼和暫存器參 與運算;而方法H則僅僅是幾句相關的彙編,程式碼更簡潔,效率更高。當然,由於編譯器的不同,可能效率的差距不大,但是,以我目前遇到的MS C ,ARM C 來看,效率的差距還是不小。

        對於以2的指數次方為"*"、"/"或"%"因子的數學運算,轉化為移位運算"<< >>"通常可以提高演算法效率。因為乘除運算指令週期通常比移位運算大。

C語言位運算除了可以提高運算效率外,在嵌入式系統的程式設計中,它的另一個最典型的應用,而且十分廣泛地正在被使用著的是位間的與(&)、或 (|)、非(~)操作,這跟嵌入式系統的程式設計特點有很大關係。我們通常要對硬體暫存器進行位設定,譬如,我們通過將AM186ER型80186處理器的中 斷遮蔽控制暫存器的第低6位設定為0(開中斷2),最通用的做法是:


#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp &~INT_I2_MASK);   

而將該位設定為1的做法是:


#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp | INT_I2_MASK);   

判斷該位是否為1的做法是:


#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
if(wTemp & INT_I2_MASK)
{
… /* 該位為1 */
}   

運用這招需要注意的是,因為CPU的不同而產生的問題。比如說,在PC上用這招編寫的程式,並在PC上除錯通過,在移植到一個16位機平臺上的時候,可能會產生程式碼隱患。所以只有在一定技術進階的基礎下才可以使用這招。


第五招:彙編嵌入

        在熟悉組合語言的人眼裡,C語言編寫的程式都是垃圾"。這種說法雖然偏激了一些,但是卻有它的道理。組合語言是效率最高的計算機語言,但是,不可能靠著它 來寫一個作業系統吧?所以,為了獲得程式的高效率,我們只好採用變通的方法--嵌入彙編,混合程式設計。嵌入式C程式中主要使用線上彙編,即在C程式中直接插 入_asm{ }內嵌彙編語句。
舉例如下,將陣列一賦值給陣列二,要求每一位元組都相符。
char string1[1024],string2[1024];

方法I

int I;
for (I =0 ;I<1024;I++)
*(string2 + I) = *(string1 + I)

方法J

#ifdef _PC_
int I;
for (I =0 ;I<1024;I++)
*(string2 + I) = *(string1 + I);
#else
#ifdef _ARM_
__asm
{
MOV R0,string1
MOV R1,string2
MOV R2,#0
loop:
LDMIA R0!, [R3-R11]
STMIA R1!, [R3-R11]
ADD R2,R2,#8
CMP R2, #400
BNE loop
}
#endif

再舉個例子:

/* 把兩個輸入引數的值相加,結果存放到另外一個全域性變數中 */
int result;
void Add(long a, long *b)
{
_asm
{
MOV AX, a
MOV BX, b
ADD AX, [BX]
MOV result, AX
}
}

方 法I是最常見的方法,使用了1024次迴圈;方法J則根據平臺不同做了區分,在ARM平臺下,用嵌入彙編僅用128次迴圈就完成了同樣的操作。這裡有朋友 會說,為什麼不用標準的記憶體拷貝函式呢?這是因為在源資料裡可能含有資料為0的位元組,這樣的話,標準庫函式會提前結束而不會完成我們要求的操作。這個例程 典型應用於LCD資料的拷貝過程。根據不同的CPU,熟練使用相應的嵌入彙編,可以大大提高程式執行的效率。

雖然是必殺技,但是如果輕易使用會付出慘重的代價。這是因為,使用了嵌入彙編,便限制了程式的可移植性,使程式在不同平臺移植的過程中,臥虎藏龍,險象環生!同時該招數也與現代軟體工程的思想相違背,只有在迫不得已的情況下才可以採用。
第六招, 使用暫存器變數
    當對一個變數頻繁被讀寫時,需要反覆訪問記憶體,從而花費大量的存取時間。為此,C語言提供了一種變數,即暫存器變數。這種變數存放在CPU的暫存器中,使 用時,不需要訪問記憶體,而直接從暫存器中讀寫,從而提高效率。暫存器變數的說明符是register。對於迴圈次數較多的迴圈控制變數及迴圈體內反覆使用 的變數均可定義為暫存器變數,而迴圈計數是應用暫存器變數的最好候選者。

(1) 只有區域性自動變數和形參才可以定義為暫存器變數。因為暫存器變數屬於動態儲存方式,凡需要採用靜態儲存方式的量都不能定義為暫存器變數,包括:模組間全域性變數、模組內全域性變數、區域性static變數;

(2) register是一個"建議"型關鍵字,意指程式建議該變數放在暫存器中,但最終該變數可能因為條件不滿足並未成為暫存器變數,而是被放在了儲存器中,但編譯器中並不報錯(在C++語言中有另一個"建議"型關鍵字:inline)。

下面是一個採用暫存器變數的例子:

/* 求1+2+3+….+n的值 */
WORD Addition(BYTE n)
{
register i,s=0;
for(i=1;i<=n;i++)
{
s=s+i;
}
return s;
}   

本程式迴圈n次,i和s都被頻繁使用,因此可定義為暫存器變數。

第七招: 利用硬體特性

首先要明白CPU對各種儲存器的訪問速度,基本上是:

CPU內部RAM > 外部同步RAM > 外部非同步RAM > FLASH/ROM

對於程式程式碼,已經被燒錄在FLASH或ROM中,我們可以讓CPU直接從其中讀取程式碼執行,但通常這不是一個好辦法,我們最好在系統啟動後將FLASH或ROM中的目的碼拷貝入RAM中後再執行以提高取指令速度;

對於UART等裝置,其內部有一定容量的接收BUFFER,我們應儘量在BUFFER被佔滿後再向CPU提出中斷。例如計算機終端在向目標機通過RS-232傳遞資料時,不宜設定UART只接收到一個BYTE就向CPU提中斷,從而無謂浪費中斷處理時間;

如果對某裝置能採取DMA方式讀取,就採用DMA讀取,DMA讀取方式在讀取目標中包含的儲存資訊較大時效率較高,其資料傳輸的基本單位是塊,而所傳輸 的資料是從裝置直接送入記憶體的(或者相反)。DMA方式較之中斷驅動方式,減少了CPU 對外設的干預,進一步提高了CPU與外設的並行操作程度。

參考文獻:
  http://purec.binghua.com/viewthread.php?tid=1318&extra=page%3D1
  http://www.51c51.net/article/show.asp?id=560