主頁 > 後端開發 > 關于C語言編程的高效學習方法,首要任務是掌握高效編程,其次乃代碼優化!

關于C語言編程的高效學習方法,首要任務是掌握高效編程,其次乃代碼優化!

2020-10-28 00:11:01 後端開發

在本篇文章中,我收集了很多經驗和方法,應用這些經驗和方法,可以幫助我們從執行速度和記憶體使用等方面來優化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是正數并且bc是個整數,那么(a/b)>c可以改寫為a>(cb),如果確定運算元是無符號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)
{
   return (p.x >= r->xmin && p.x < r->xmax &&
                      p.y >= r->ymin && p.y < r->ymax);
}

這里有一種更快的方法:x>min && x<max可以轉換為(unsigned)(x-min)<(max-min),這對于min等于0時更為有益,優化后的代碼如下:

bool PointInRectangelArea (Point p, Rectangle *r)
{
    return ((unsigned) (p.x - r->xmin) < r->xmax &&
   (unsigned) (p.y - r->ymin) < r->ymax);
 
}

布爾運算式和零值比較

處理器的標志位在比較指令操作后被設定,標志位同樣可以被諸如MOV、ADD、AND、MUL等基本算術和裸機指令改寫,如果資料指令設定了標志位,N和Z標志位也將與結果與0比較一樣進行設定,N標志表示結果是否是負值,Z標志表示結果是否是0,

C語言中,處理器中的N和Z標志位與下面的指令聯系在一起:有符號關系運算x<0,x>=0,x==0,x!=0;無符號關系運算x==0,x!=0(或者x>0),

C代碼中每次關系運算子的呼叫,編譯器都會發出一個比較指令,如果運算子是上面提到的,編譯器便會優化掉比較指令,例如:

int aFunction(int x, int y)
{
   if (x + y < 0)
      return 1;
  else
     return 0;
}

 

盡可能的使用上面的判斷方式,這可以在關鍵回圈中減少比較指令的呼叫,進而減少代碼體積并提高代碼性能,C語言沒有借位和溢位位的概念,因此,如果不借助匯編,不可能直接使用借位標志C和溢位位標志V,但編譯器支持借位(無符號溢位),例如:

 

int sum(int x, int y)
{
   int res;
   res = x + y;
   if ((unsigned) res < (unsigned) x) // carry set?  //
     res++;
   return res;
}

懶檢測開發

在if(a>10 && b=4)這樣的陳述句中,確保AND運算式的第一部分最可能較快的給出結果(或者最早、最快計算),這樣第二部分便有可能不需要執行,

用switch()函式替代if…else…

對于涉及if…else…else…這樣的多條件判斷,例如:

if( val == 1)
    dostuff1();
else if (val == 2)
    dostuff2();
else if (val == 3)
    dostuff3();

使用switch可能更快:

switch( val )
{
    case 1: dostuff1(); break;

    case 2: dostuff2(); break;

    case 3: dostuff3(); break;
}

 

在if()陳述句中,如果最后一條陳述句命中,之前的條件都需要被測驗執行一次,Switch允許我們不欄位外的測驗,如果必須使用if…else…陳述句,將最可能執行的放在最前面,

二分中斷

使用二分方式中斷代碼而不是讓代碼堆成一列,不要像下面這樣做:

if(a==1) {
} else if(a==2) {
} else if(a==3) {
} else if(a==4) {
} else if(a==5) {
} else if(a==6) {
} else if(a==7) {
} else if(a==8)

{
}

使用下面的二分方式替代它,如下:

if(a<=4) {
    if(a==1)     {
    }  else if(a==2)  {
    }  else if(a==3)  {
    }  else if(a==4)   {

    }
}
else
{
    if(a==5)  {
    } else if(a==6)   {
    } else if(a==7)  {
    } else if(a==8)  {
    }
}

或者如下:

if(a<=4)
{
    if(a<=2)
    {
        if(a==1)
        {
            /* a is 1 */
        }
        else
        {
            /* a must be 2 */
        }
    }
    else
    {
        if(a==3)
        {
            /* a is 3 */
        }
        else
        {
            /* a must be 4 */
        }
    }
}
else
{
    if(a<=6)
    {
        if(a==5)
        {
            /* a is 5 */
        }
        else
        {
            /* a must be 6 */
        }
    }
    else
    {
        if(a==7)
        {
            /* a is 7 */
        }
        else
        {
            /* a must be 8 */
        }
    }
}

比較如下兩種case陳述句:

switch陳述句vs查找表

Switch的應用場景如下:

  • 呼叫一到多個函式

  • 設定變數值或者回傳一個值

  • 執行一到多個代碼片段

如果case標簽很多,在switch的前兩個使用場景中,使用查找表可以更高效的完成,例如下面的兩種轉換字串的方式:

char * Condition_String1(int condition) {
  switch(condition) {
     case 0: return "EQ";
     case 1: return "NE";
     case 2: return "CS";
     case 3: return "CC";
     case 4: return "MI";
     case 5: return "PL";
     case 6: return "VS";
     case 7: return "VC";
     case 8: return "HI";
     case 9: return "LS";
     case 10: return "GE";
     case 11: return "LT";
     case 12: return "GT";
     case 13: return "LE";
     case 14: return "";
     default: return 0;
  }
}
 
char * Condition_String2(int condition) {
   if ((unsigned) condition >= 15) return 0;
      return
      "EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" +
       3 * condition;
}

第一個程式需要240 bytes,而第二個僅僅需要72 bytes,

回圈

回圈是大多數程式中的常用的結構;程式執行的大部分時間發生在回圈中,因此十分值得在回圈執行時間上下一番功夫,

 

回圈終止

如果不加注意,回圈終止條件的撰寫會導致額外的負擔,我們應該使用計數到零的回圈和簡單的回圈終止條件,簡單的終止條件消耗更少的時間,看下面計算n!的兩個程式,第一個實作使用遞增的回圈,第二個實作使用遞級訓圈,

 

int fact1_func (int n)
{
    int i, fact = 1;
    for (i = 1; i <= n; i++)
      fact *= i;
    return (fact);
}
 
int fact2_func(int n)
{
    int i, fact = 1;
    for (i = n; i != 0; i--)
       fact *= i;
    return (fact);
}

第二個程式的fact2_func執行效率高于第一個,

 

更快的for()回圈

這是一個簡單而高效的概念,通常,我們撰寫for回圈代碼如下:

for( i=0;  i<10;  i++){ ... }

i從0回圈到9,如果我們不介意回圈計數的順序,我們可以這樣寫:

for( i=10; i--; ) { ... }

這樣快的原因是因為它能更快的處理i的值–測驗條件是:i是非零的嗎?如果這樣,遞減i的值,對于上面的代碼,處理器需要計算“計算i減去10,其值非負嗎?如果非負,i遞增并繼續”,簡單的回圈卻有很大的不同,這樣,i從9遞減到0,這樣的回圈執行速度更快,

這里的語法有點奇怪,但確實合法的,回圈中的第三條陳述句是可選的(無限回圈可以寫為for(;;)),如下代碼擁有同樣的效果:

for(i=10; i; i--){}

或者更進一步的:

for(i=10; i!=0; i--){}

 

這里我們需要記住的是回圈必須終止于0(因此,如果在50到80之間回圈,這不會起作用),并且回圈計數器是遞減的,使用遞增回圈計數器的代碼不享有這種優化,

 

合并回圈

如果一個回圈能解決問題堅決不用二個,但如果你需要在回圈中做很多作業,這坑你并不適合處理器的指令快取,這種情況下,兩個分開的回圈可能會比單個回圈執行的更快,下面是一個例子:

 

函式回圈

呼叫函式時總是會有一定的性能消耗,不僅程式指標需要改變,而且使用的變數需要壓堆疊并分配新變數,為提升程式的性能,在函式這點上有很多可以優化的,在保持程式代碼可讀性的同時也需要代碼的大小是可控的,

如果在回圈中一個函式經常被呼叫,那么就將回圈納入到函式中,這樣可以減少重復的函式呼叫,代碼如下:

for(i=0 ; i<100 ; i++)
{
    func(t,i);
}
-
-
-
void func(int w,d)
{
    lots of stuff.
}

應改為:

func(t);
-
-
-
void func(w)
{
    for(i=0 ; i<100 ; i++)
    {
        //lots of stuff.
    }
}

 

回圈展開

簡單的回圈可以展開以獲取更好的性能,但需要付出代碼體積增加的代價,回圈展開后,回圈計數應該越來越小從而執行更少的代碼分支,如果回圈迭代次數只有幾次,那么可以完全展開回圈,以便消除循壞帶來的負擔,

這會帶來很大的不同,回圈展開可以帶非常可觀的節省性能,原因是代碼不用每次回圈需要檢查和增加i的值,例如:

編譯器通常會像上面那樣展開簡單的,迭代次數固定的回圈,但是像下面的代碼:

for(i=0;i< limit;i++) { ... }

下面的代碼(Example 1)明顯比使用回圈的方式寫的更長,但卻更有效率,block-sie的值設定為8僅僅適用于測驗的目的,只要我們重復執行“loop-contents”相同的次數,都會有很好的效果,

在這個例子中,回圈條件每8次迭代才會被檢查,而不是每次都進行檢查,由于不知道迭代的次數,一般不會被展開,因此,盡可能的展開回圈可以讓我們獲得更好的執行速度,

//Example 1
 
#include<STDIO.H>
 
#define BLOCKSIZE (8)
 
void main(void)
{
int i = 0;
int limit = 33;  /* could be anything */
int blocklimit;
 
/* The limit may not be divisible by BLOCKSIZE,
 * go as near as we can first, then tidy up.
 */
blocklimit = (limit / BLOCKSIZE) * BLOCKSIZE;
 
/* unroll the loop in blocks of 8 */
while( i < blocklimit )
{
    printf("process(%d)\n", i);
    printf("process(%d)\n", i+1);
    printf("process(%d)\n", i+2);
    printf("process(%d)\n", i+3);
    printf("process(%d)\n", i+4);
    printf("process(%d)\n", i+5);
    printf("process(%d)\n", i+6);
    printf("process(%d)\n", i+7);
 
    /* update the counter */
    i += 8;
 
}
 
/*
 * There may be some left to do.
 * This could be done as a simple for() loop,
 * but a switch is faster (and more interesting)
 */
 
if( i < limit )
{
    /* Jump into the case at the place that will allow
     * us to finish off the appropriate number of items.
     */
 
    switch( limit - i )
    {
        case 7 : printf("process(%d)\n", i); i++;
        case 6 : printf("process(%d)\n", i); i++;
        case 5 : printf("process(%d)\n", i); i++;
        case 4 : printf("process(%d)\n", i); i++;
        case 3 : printf("process(%d)\n", i); i++;
        case 2 : printf("process(%d)\n", i); i++;
        case 1 : printf("process(%d)\n", i);
    }
}
 
}

 

統計非零位的數量

通過不斷的左移,提取并統計最低位,示例程式1高效的檢查一個陣列中有幾個非零位,示例程式2被回圈展開四次,然后通過將四次移位合并成一次來優化代碼,經常展開回圈,可以提供很多優化的機會,

//Example - 1

int countbit1(uint n)
{
  int bits = 0;
  while (n != 0)
  {
    if (n & 1) bits++;
    n >>= 1;
   }
  return bits;
}

//Example - 2

int countbit2(uint n)
{
   int bits = 0;
   while (n != 0)
   {
      if (n & 1) bits++;
      if (n & 2) bits++;
      if (n & 4) bits++;
      if (n & 8) bits++;
      n >>= 4;
   }
   return bits;
}

 

盡早的斷開回圈

通常,回圈并不需要全部都執行,例如,如果我們在從陣列中查找一個特殊的值,一經找到,我們應該盡可能早的斷開回圈,例如:如下回圈從10000個整數中查找是否存在-99,

 

found = FALSE;
for(i=0;i<10000;i++)
{
    if( list[i] == -99 )
    {
        found = TRUE;
    }
}
 
if( found ) 
    printf("Yes, there is a -99. Hooray!\n");

 

上面的代碼可以正常作業,但是需要回圈全部執行完畢,而不論是否我們已經查找到,更好的方法是一旦找到我們查找的數字就終止繼續查詢,

 

found = FALSE;
for(i=0; i<10000; i++)
{
    if( list[i] == -99 )
    {
        found = TRUE;
        break;
    }
}
if( found ) 
    printf("Yes, there is a -99. Hooray!\n");

假如待查資料位于第23個位置上,程式便會執行23次,從而節省9977次回圈,

函式設計

設計小而簡單的函式是個很好的習慣,這允許暫存器可以執行一些諸如暫存器變數申請的優化,是非常高效的,

 

函式呼叫的性能消耗

函式呼叫對于處理器的性能消耗是很小的,只占有函式執行作業中性能消耗的一小部分,引數傳入函式變數暫存器中有一定的限制,這些引數必須是整型兼容的(char,shorts,ints和floats都占用一個字)或者小于四個字大小(包括占用2個字的doubles和long longs),

如果引數限制個數為4,那么第五個和之后的字就會存盤在堆疊上,這便在呼叫函式是需要從堆疊上加載引數從而增加存盤和讀取的消耗,

看下面的代碼:

int f1(int a, int b, int c, int d) {
   return a + b + c + d;
}
 
int g1(void) {
   return f1(1, 2, 3, 4);
}
 
int f2(int a, int b, int c, int d, int e, int f) {
  return a + b + c + d + e + f;
}
 
ing g2(void) {
 return f2(1, 2, 3, 4, 5, 6);
}

函式g2中的第五個和第六個引數存盤于堆疊上并在函式f2中進行加載,會多消耗2個引數的存盤,

 

減少函式引數傳遞消耗

減少函式引數傳遞消耗的方法有:

  • 盡量保證函式使用少于四個引數,這樣就不會使用堆疊來存盤引數值,

  • 如果函式需要多于四個的引數,盡量確保使用后面引數的價值高于讓其存盤于堆疊所付出的代價,

  • 通過指標傳遞引數的參考而不是傳遞引數結構體本身,

  • 將引數放入一個結構體并通過指標傳入函式,這樣可以減少引數的數量并提高可讀性,

  • 盡量少用占用兩個字大小的long型別引數,對于需要浮點型別的程式,double也因為占用兩個字大小而應盡量少用,

  • 避免函式引數既存在于暫存器又存在于堆疊中(稱之為引數拆分),現在的編譯器對這種情況處理的不夠高效:所有的暫存器變數也會放入到堆疊中,

  • 避免變參,變參函式將引數全部放入堆疊,

 

 

葉子函式

不呼叫任何函式的函式稱之為葉子函式,在以下應用中,近一半的函式呼叫是呼叫葉子函式,由于不需要執行暫存器變數的存盤和讀取,葉子函式在任何平臺都很高效,

暫存器變數讀取的性能消耗,相比于使用四五個暫存器變數的葉子函式所做的作業帶來的系能消耗是非常小的,所以盡可能的將經常呼叫的函式寫成葉子函式,函式呼叫的次數可以通過一些工具檢查,

下面是一些將一個函式編譯為葉子函式的方法:

  • 避免呼叫其他函式:包括那些轉而呼叫C庫的函式(比如除法或者浮點數操作函式),

  • 對于簡短的函式使用__inline修飾(),

行內函式

行內函式禁用所有的編譯選項,使用__inline修飾函式導致函式在呼叫處直接替換為函式體,這樣代碼呼叫函式更快,但增加代碼的大小,特別在函式本身比較大而且經常呼叫的情況下,

__inline int square(int x) {
   return x * x;
}
 
#include <MATH.H>
 
double length(int x, int y){
    return sqrt(square(x) + square(y));
}

使用行內函式的好處如下:

  • 沒有函式呼叫負擔,函式呼叫處直接替換為函式體,因此沒有諸如讀取暫存器變數等性能消耗,

  • 更小的引數傳遞消耗,由于不需要拷貝變數,傳遞引數的消耗更小,如果引數是常量,編譯器可以提供更好的優化,

行內函式的缺陷是如果呼叫的地方很多,代碼的體積會變得很大,這主要取決于函式本身的大小和呼叫的次數,

僅對重要的函式使用inline是明智的,如果使用得當,行內函式甚至可以減少代碼的體積:函式呼叫會產生一些計算機指令,但是使用行內的優化版本可能產生更少的計算機指令,

使用查找表

函式通常可以設計成查找表,這樣可以顯著提升性能,查找表的精確度比通常的計算低,但對于一般的程式并沒什么差異,

許多信號處理程式(例如,調制解調器解調軟體)使用很多非常消耗計算性能的sin和cos函式,對于實時系統,精確性不是特別重要,sin、cos查找表可能更合適,當使用查找表時,盡可能將相似的操作放入查找表,這樣比使用多個查找表更快,更能節省存盤空間,

浮點運算

盡管浮點運算對于所有的處理器都很耗時,但對于實作信號處理軟體時我們仍然需要使用,在撰寫浮點操作程式時,記住如下幾點:

  • 浮點除法很慢,浮點除法比加法或者乘法慢兩倍,通過使用常量將除法轉換為乘法(例如,x=x/3.0可以替換為x=x*(1.0/3.0)),常量的除法在編譯期間計算,

  • 使用float代替double,Float型別的變數消耗更好的記憶體和暫存器,并由于精度低而更加高效,如果精度夠用,盡可能使用float,

  • 避免使用先驗函式,先驗函式,例如sin、exp和log是通過一系列的乘法和加法實作的(使用了精度擴展),這些操作比通常的乘法至少慢十倍,

  • 簡化浮點運算運算式,編譯器并不能將應用于整型操作的優化手段應用于浮點操作,例如,3*(x/3)可以優化為x,而浮點運算就會損失精度,因此,如果知道結果正確,進行必要手工浮點優化是有必要的,

然而,浮點運算的表現可能不能滿足特定軟體對性能的需求,這種情況下,最好的辦法或許是使用定點算數運算,當值的范圍足夠小,定點算數操作比浮點運算更精確、更快速,

其他技巧

通常,可以使用空間換時間,如果你能快取經常用的資料而不是重新計算,這便能更快的訪問,比如sine和cosine查找表,或者偽亂數,

  • 盡量不在回圈中使用++和–,例如:while(n–){},這有時難于優化,

  • 減少全域變數的使用,

  • 除非像宣告為全域變數,使用static修飾變數為檔案內訪問,

  • 盡可能使用一個字大小的變數(int、long等),使用它們(而不是char,short,double,位域等)機器可能運行的更快,

  • 不使用遞回,遞回可能優雅而簡單,但需要太多的函式呼叫,

  • 不在回圈中使用sqrt開平方函式,計算平方根非常消耗性能,

  • 一維陣列比多維陣列更快,

  • 編譯器可以在一個檔案中進行優化-避免將相關的函式拆分到不同的檔案中,如果將它們放在一起,編譯器可以更好的處理它們(例如可以使用inline),

  • 單精度函式比雙精度更快,

  • 浮點乘法運算比浮點除法運算更快-使用val*0.5而不是val/2.0,

  • 加法操作比乘法快-使用val+val+val而不是val*3,

  • put()函式比printf()快,但不靈活,

  • 使用#define宏取代常用的小函式,

  • 二進制/未格式化的檔案訪問比格式化的檔案訪問更快,因為程式不需要在人為可讀的ASCII和機器可讀的二進制之間轉化,如果你不需要閱讀檔案的內容,將它保存為二進制,

  • 如果你的庫支持mallopt()函式(用于控制malloc),盡量使用它,MAXFAST的設定,對于呼叫很多次malloc作業的函式由很大的性能提升,如果一個結構一秒鐘內需要多次創建并銷毀,試著設定mallopt選項,

最后,但是是最重要的是-將編譯器優化選項打開!看上去很顯而易見,但卻經常在產品推出時被忘記,編譯器能夠在更底層上對代碼進行優化,并針對目標處理器執行特定的優化處理,

 

 

 

如果你對C/C++感興趣,想學編程,小編推薦一個C/C++技術交流群【點擊進入】!

 

一個活躍、高逼格、高層次的編程學習殿堂;編程入門只是順帶,思維的提高才有價值!

 

涉及到了編程入門、游戲編程、網路編程、Windows編程、Linux編程、Qt界面開發、黑客等等......

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/194386.html

標籤:C

上一篇:「有數可據」選擇IT行業的1000個理由!

下一篇:【Flutter 混合開發】添加 Flutter 到 Android Activity

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more