1. 程式人生 > >C語言高效程式設計與程式碼優化

C語言高效程式設計與程式碼優化

在本篇文章中,我收集了很多經驗和方法。應用這些經驗和方法,可以幫助我們從執行速度和記憶體使用等方面來優化C語言程式碼。

簡介

在最近的一個專案中,我們需要開發一個執行在移動裝置上但不保證影象高質量的輕量級JPEG庫。期間,我總結了一些讓程式執行更快的方法。在本篇文章中,我收集了一些經驗和方法。應用這些經驗和方法,可以幫助我們從執行速度和記憶體使用等方面來優化C語言程式碼。

儘管在C程式碼優化方面有很多的指南,但是關於編譯和你使用的程式設計機器方面的優化知識卻很少。

通常,為了讓你的程式執行的更快,程式的程式碼量可能需要增加。程式碼量的增加又可能會對程式的複雜度和可讀性帶來不利的影響。這對於在手機、PDA等對於記憶體使用有很多限制的小型裝置上編寫程式時是不被允許的。因此,在程式碼優化時,我們的座右銘應該是確保記憶體使用和執行速度兩方面都得到優化。

宣告

實際上,在我的專案中,我使用了很多優化ARM程式設計的方法(該專案是基於ARM平臺的),也使用了很多網際網路上面的方法。但並不是所有文章提到的方法都能起到很好的作用。所以,我對有用的和高效的方法進行了總結收集。同時,我還修改了其中的一些方法,使他們適用於所有的程式設計環境,而不是侷限於ARM環境。

哪裡需要使用這些方法?

沒有這一點,所有的討論都無從談起。程式優化最重要的就是找出待優化的地方,也就是找出程式的哪些部分或者哪些模組執行緩慢亦或消耗大量的記憶體。只有程式的各部分經過了優化,程式才能執行的更快。

程式中執行最多的部分,特別是那些被程式內部迴圈重複呼叫的方法最該被優化。

對於一個有經驗的碼農,發現程式中最需要被優化的部分往往很簡單。此外,還有很多工具可以幫助我們找出需要優化的部分。我使用過Visual C++內建的效能工具profiler來找出程式中消耗最多記憶體的地方。另一個我使用過的工具是英特爾的Vtune,它也能很好的檢測出程式中執行最慢的部分。根據我的經驗,內部或巢狀迴圈,呼叫第三方庫的方法通常是導致程式執行緩慢的最主要的起因。

整形數

如果我們確定整數非負,就應該使用unsigned int而不是int。有些處理器處理無符號unsigned 整形數的效率遠遠高於有符號signed整形數(這是一種很好的做法,也有利於程式碼具體型別的自解釋)。

因此,在一個緊密迴圈中,宣告一個int整形變數的最好方法是:

register unsigned int variable_name;

記住,整形in的運算速度高浮點型float,並且可以被處理器直接完成運算,而不需要藉助於FPU(浮點運算單元)或者浮點型運算庫。儘管這不保證編譯器一定會使用到暫存器儲存變數,也不能保證處理器處理能更高效處理unsigned整型,但這對於所有的編譯器是通用的。

例如在一個計算包中,如果需要結果精確到小數點後兩位,我們可以將其乘以100,然後儘可能晚的把它轉換為浮點型數字。

除法和取餘數

在標準處理器中,對於分子和分母,一個32位的除法需要使用20至140次迴圈操作。除法函式消耗的時間包括一個常量時間加上每一位除法消耗的時間。

Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator)
     = C0 + C1 * (log2 (numerator) - log2 (denominator)).

對於ARM處理器,這個版本需要20+4.3N次迴圈。這是一個消耗很大的操作,應該儘可能的避免執行。有時,可以通過乘法表達式來替代除法。例如,假如我們知道b是正數並且b*c是個整數,那麼(a/b)>c可以改寫為a>(c*b)。如果確定運算元是無符號unsigned的,使用無符號unsigned除法更好一些,因為它比有符號signed除法效率高。

合併除法和取餘數

在一些場景中,同時需要除法(x/y)和取餘數(x%y)操作。這種情況下,編譯器可以通過呼叫一次除法操作返回除法的結果和餘數。如果既需要除法的結果又需要餘數,我們可以將它們寫在一起,如下所示:

int func_div_and_mod (int a, int b) { 
        return (a / b) + (a % b);
    }

通過2的冪次進行除法和取餘數

如果除法中的除數是2的冪次,我們可以更好的優化除法。編譯器使用移位操作來執行除法。因此,我們需要儘可能的設定除數為2的冪次(例如64而不是66)。並且依然記住,無符號unsigned整數除法執行效率高於有符號signed整形出發。

typedef unsigned int uint;    
uint div32u (uint a) {
return a / 32; }
int div32s (int a){
return a / 32; }

上面兩種除法都避免直接呼叫除法函式,並且無符號unsigned的除法使用更少的計算機指令。由於需要移位到0和負數,有符號signed的除法需要更多的時間執行。

取模的一種替代方法

我們使用取餘數操作符來提供算數取模。但有時可以結合使用if語句進行取模操作。考慮如下兩個例子:

uint modulo_func1 (uint count)
{   return (++count % 60);
}

uint modulo_func2 (uint count)
{   if (++count >= 60)  count = 0;  
return (count); }

優先使用if語句,而不是取餘數運算子,因為if語句的執行速度更快。這裡注意新版本函式只有在我們知道輸入的count結餘0至59時在能正確的工作。

使用陣列下標

如果你想給一個變數設定一個代表某種意思的字元值,你可能會這樣做:

switch ( queue ) {case 0 :   
letter = 'W';
break;case 1 :
letter = 'S';
break;case 2 :
letter = 'U';
break; }

或者這樣做:

if ( queue == 0 )
  letter = 'W';else if ( queue == 1 )
  letter = 'S';else
  letter = 'U';

一種更簡潔、更快的方法是使用陣列下標獲取字元陣列的值。如下:

static char *classes="WSU";

letter = classes[queue];

全域性變數

全域性變數絕不會位於暫存器中。使用指標或者函式呼叫,可以直接修改全域性變數的值。因此,編譯器不能將全域性變數的值快取在暫存器中,但這在使用全域性變數時便需要額外的(常常是不必要的)讀取和儲存。所以,在重要的迴圈中我們不建議使用全域性變數。

如果函式過多的使用全域性變數,比較好的做法是拷貝全域性變數的值到區域性變數,這樣它才可以存放在暫存器。這種方法僅僅適用於全域性變數不會被我們呼叫的任意函式使用。例子如下:

int f(void);int g(void);int 
errs;void test1(void){ errs += f(); errs += g(); }void test2(void){
int localerrs = errs; localerrs += f(); localerrs += g(); errs = localerrs; }

注意,test1必須在每次增加操作時載入並存儲全域性變數errs的值,而test2儲存localerrs於暫存器並且只需要一個計算機指令。

使用別名

考慮如下的例子:

void func1( int *data ){    
int i;
for(i=0; i<10; i++) {
anyfunc( *data, i); } }

儘管*data的值可能從未被改變,但編譯器並不知道anyfunc函式不會修改它,所以程式必須在每次使用它的時候從記憶體中讀取它。如果我們知道變數的值不會被改變,那麼就應該使用如下的編碼:

void func1( int *data ){    
int i;
int localdata; localdata = *data;
for(i=0; i<10; i++) { anyfunc ( localdata, i); } }

這為編譯器優化程式碼提供了條件。

變數的生命週期分割

由於處理器中暫存器是固定長度的,程式中數字型變數在暫存器中的儲存是有一定限制的。

有些編譯器支援“生命週期分割”(live-range splitting),也就是說在程式的不同部分,變數可以被分配到不同的暫存器或者記憶體中。變數的生命週期開始於對它進行的最後一次賦值,結束於下次賦值前的最後一次使用。在生命週期內,變數的值是有效的,也就是說變數是活著的。不同生命週期之間,變數的值是不被需要的,也就是說變數是死掉的。這樣,暫存器就可以被其餘變數使用,從而允許編譯器分配更多的變數使用暫存器。

需要使用暫存器分配的變數數目需要超過函式中不同變數生命週期的個數。如果不同變數生命週期的個數超過了暫存器的數目,那麼一些變數必須臨時儲存於記憶體。這個過程就稱之為分割。

編譯器首先分割最近使用的變數,用以降低分割帶來的消耗。禁止變數生命週期分割的方法如下:

  • 限定變數的使用數量:這個可以通過保持函式中的表示式簡單、小巧、不使用太多的變數實現。將較大的函式拆分為小而簡單的函式也會達到很好的效果。

  • 對經常使用到的變數採用暫存器儲存:這樣允許我們告訴編譯器該變數是需要經常使用的,所以需要優先儲存於暫存器中。然而,在某種情況下,這樣的變數依然可能會被分割出暫存器。

變數型別

C編譯器支援基本型別:char、short、int、long(包括有符號signed和無符號unsigned)、float和double。使用正確的變數型別至關重要,因為這可以減少程式碼和資料的大小並大幅增加程式的效能。

區域性變數

我們應該儘可能的不使用char和short型別的區域性變數。對於char和short型別,編譯器需要在每次賦值的時候將區域性變數減少到8或者16位。這對於有符號變數稱之為有符號擴充套件,對於無符號變數稱之為零擴充套件。這些擴充套件可以通過暫存器左移24或者16位,然後根據有無符號標誌右移相同的位數實現,這會消耗兩次計算機指令操作(無符號char型別的零擴充套件僅需要消耗一次計算機指令)。

可以通過使用int和unsigned int型別的區域性變數來避免這樣的移位操作。這對於先載入資料到區域性變數,然後處理區域性變數資料值這樣的操作非常重要。無論輸入輸出資料是8位或者16位,將它們考慮為32位是值得的。

考慮下面的三個函式:

int wordinc (int a){   
return a + 1; }short shortinc (short a){
return a + 1; }char charinc (char a){
return a + 1; }

儘管結果均相同,但是第一個程式片段執行速度高於後兩者。

指標

我們應該儘可能的使用引用值的方式傳遞結構資料,也就是說使用指標,否則傳遞的資料會被拷貝到棧中,從而降低程式的效能。我曾見過一個程式採用傳值的方式傳遞非常大的結構資料,然後這可以通過一個簡單的指標更好的完成。

函式通過引數接受結構資料的指標,如果我們確定不改變資料的值,我們需要將指標指向的內容定義為常量。例如:

void print_data_of_a_structure ( const Thestruct  *data_pointer){
    ...printf contents of the structure...
}

這個示例告訴編譯器函式不會改變外部引數的值(使用const修飾),並且不用在每次訪問時都進行讀取。同時,確保編譯器限制任何對只讀結構的修改操作從而給予結構資料額外的保護。

指標鏈

指標鏈經常被用於訪問結構資料。例如,常用的程式碼如下:

typedef struct { int x, y, z; } Point3;typedef struct { Point3 *pos, *direction; } Object;void InitPos1(Object *p){
   p->pos->x = 0;
   p->pos->y = 0;
   p->pos->z = 0;
}

然而,這種的程式碼在每次操作時必須重複呼叫p->pos,因為編譯器不知道p->pos->x與p->pos是相同的。一種更好的方法是快取p->pos到一個區域性變數:

void InitPos2(Object *p)
{
   Point3 *pos = p->pos;   
pos->x = 0;
pos->y = 0;
pos->z = 0; }

另一種方法是在Object結構中直接包含Point3型別的資料,這能完全消除對Point3使用指標操作。

條件執行

條件執行語句大多在if語句中使用,也在使用關係運算符(<,==,>等)或者布林值表示式(&&,!等)計算複雜表示式時使用。對於包含函式呼叫的程式碼片段,由於函式返回值會被銷燬,因此條件執行是無效的。

因此,保持if和else語句儘可能簡單是十分有益處的,因為這樣編譯器可以集中處理它們。關係表示式應該寫在一起。

下面的例子展示編譯器如何使用條件執行:

int g(int a, int b, int c, int d){   
if (a > 0 && b > 0 && c < 0 && d < 0)
//  grouped conditions tied up together// return a + b + c + d;
return -1; }

由於條件被聚集到一起,編譯器能夠將他們集中處理。

布林表示式和範圍檢查

一個常用的布林表示式是用於判斷變數是否位於某個範圍內,例如,檢查一個圖形座標是否位於一個視窗內:

bool PointInRectangelArea (Point p, Rectangle *r)
{   
            
           

相關推薦

C語言高效程式設計程式碼優化

在本篇文章中,我收集了很多經驗和方法。應用這些經驗和方法,可以幫助我們從執行速度和記憶體使用等方面來優化C語言程式碼。 簡介 在最近的一個專案中,我們需要開發一個執行在移動裝置上但不保證影象高質量的輕量級JPEG庫。期間,我總結了一些讓程式執行更快的方法。

C語言高效程式設計的的四大絕招

編寫高效簡潔的C語言程式碼,是許多軟體工程師追求的目標。本文就工作中的一些體會和經驗做相關的闡述,不對的地方請各位指教。  第一招:以空間換時間  計算機程式中最大的矛盾是空間和時間的矛盾,那麼,從這個角度出發逆向思維來考慮程式的效率問題,我們就有了解決問題的第1招--以空間

C語言網路程式設計程式碼

首先 project->settings->link 在object/library modules 中加入ws2_32.lib檔案 在專案上點右鍵->屬性->配置屬性->

ARM彙編C語言混合程式設計之彙編呼叫C函式

呼叫沒有引數的函式 呼叫有引數的函式 總結 本文所用硬體平臺為S3C2440開發板。通過一個點亮數碼管的程式說明ARM彙編呼叫C函式的方法。 根據C語言中函式引數的個數,可以將彙編呼叫C函式分為兩種情況,呼叫沒有引數的函式和呼叫有引數的

C++ 高效程式設計程式碼規範

本文將一些常用的程式碼註釋、命名等規範總結下,良好的程式碼規範不僅讓自己便於修改閱讀,對程式碼維護也是很重要的。 1. 版本和版本宣告 版本和版本檔案宣告位於標頭檔案和定義檔案的開頭,主要內容 (1)版本資訊 (2)檔名稱、識別符號、摘要 (3)當前的版本號、作

C語言程式設計》實踐專案——二維陣列指標

【專案1-二維陣列當函式引數】定義一個函式來完成對引數陣列中元素的求和工作,函式宣告如下:int sum(int array[ ][4],int m,int n); //該函式完成對array陣列中的前m行和n列元素求和在以下程式的基礎上,完成對sum函式的定義。#include <stdio.h&g

基於組合語言c/c++語言混合程式設計程式設計研究(一)

組合語言的特點在於佔用的空間小,執行的速度快,是面向機器的一種語言,在某些場合具有無可替代的作用。其不足之處在於較為高階的語言程式編寫存在一定難度,在處理資料時這一點體現的更加明顯。C++是一種高階語言,功能豐富,表達靈活,開發高效,在應用性方面要勝過組合語言。但是在考慮到

C語言高效編程的幾招(絕對實用,絕對經典)

n) 工程 fine bit 高效 nbsp 與運算 測試的 body 編寫高效簡潔的C語言代碼,是許多軟件工程師追求的目標。廢話不說,走起! 第一招:以空間換時間 計算機程序中最大的矛盾是空間和時間的矛盾,那麽,從這個角度出發逆向思維來考慮程序的效率問題 eg.字

C語言基礎--決策判斷

出現 program 程序 c語言基礎 res else if express 基礎 裏的 需要掌握的內容: 一. 1.if語句 C語言提供一種稱為if語句的語法格式,通用的形式是: if(express) { programming statements; }

散分+快來報名“C語言指針匯編內存地址(二)”公開課

公開 lan shuf target dsd blank dmg userinfo gin iR歡2kaq崖居止40攘http://www.docin.com/app/user/userinfo?userid=179252984 Qsgb5V1仗mchttp://t.doc

1.2 C語言--函數數組

詳細 cor 行高 ret mar 編譯 自動變 如果 模塊化 函數 函數的定義 返回值類型函數名(類型形參名[,……]){ 函數體 } 除了沒有訪問修飾符外,基本等同於java的函數。 良好的程序設計風格要求即使沒有返回值,也要使用return;作為最後一條

C語言的宣告定義

keil的專案中,遇到呼叫其他C檔案函式和變數的情況: 對於函式,在a.c下面進行編寫,之後在a.h下面進行宣告,其他檔案包含a.h即可呼叫。 對於變數,在a.c下面進行定義,在a.h下面也要進行一下宣告,其他檔案使用此變數時,包含a.h即可使用。 關於變數的定義與宣告 變數定義即為

C語言檔案操作Python檔案操作的比較

無論是在C語言中還是在Python中,對檔案的操作都是直接實現了計算機內部與外部之間的互動; 通過將資料寫入檔案可以更好地實現資料的儲存,而不會在程式結束之後資料隨之消失, 而通過檔案的讀取操作則可以更好地避免自己講大量資料的手動輸入程式。   本篇文章主要介紹C語言以

C語言程式設計實現兩個矩陣

輸入一個3乘4矩陣✖️4乘5矩陣,輸出一個3乘5的矩陣 #include<stdio.h> int main() { int matrix1[3][4],matrix2[4][5],matrix3[3][5]; int i,j,k; int jz3[

C語言實現卷積程式碼

C語言實現一維卷積程式碼(失敗)待修改 #include<iostream> using namespace std; int main() { int i,j,k,n,m,a[n],b[m],c[n+m-1]; cout<<"輸入n:"; cin>>n

C語言實現魔方陣程式碼及解析

問題描述編寫程式,實現如下表所示的5-魔方陣。 17 24 1 8 15 23 5 7 14 16 4 6 13 20 22

C語言程式設計 實踐參考 迴圈的巢狀流程圖

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

C語言程式設計 實踐參考 分段函式求值switch語句版

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

C語言核心程式設計-夏曹俊-專題視訊課程

C語言核心程式設計—168人已學習 課程介紹         C語言並不是一個高階語言,它實際上屬於高階語言與低階語言之間的中間語言,它直接與記憶體打交道,豐富的資料型別、運算子,但是C語言絕非是一

C語言有符號符號引數的比較

 1.整型無符號引數與有符號引數比較 程式碼段: #include<cstdio> int main() { int a=-1; unsigned int b=0; if(b>a)printf("b>a\n"); else printf("b<a\