c/c++代碼性能效率


一、儘量減少值傳遞,多用引用來傳遞參數

boolCompare(string s1, string s2)
boolCompare(string *s1, string *s2)
boolCompare(string &s1, string &s2)
boolCompare(const string &s1, const string &s2)

  其中若使用第一個函數(值傳遞),則在參數傳遞和函數返回時,需要調用string的構造函數和析構函數兩次(即共多調用了四個函數),而其他的三個函數(指針傳遞和引用傳遞)則不需要調用這四個函數。因爲指針和引用都不會創建新的對象。如果一個構造一個對象和析構一個對象的開銷是龐大的,這就是會效率造成一定的影響。

  引用是一個變量的別名,對其操作等同於對實際對象操作,所以當你確定在你的函數是不會或不需要變量參數的值時,就大膽地在聲明的前面加上一個const吧,就如最後的一個函數聲明一樣。同時加上一個const還有一個好處,就是可以對常量進行引用,若不加上const修飾符,引用是不能引用常量的。

二、循環

  循環內定義,還是循環外定義對象:

  如果調用賦值操作函數的開銷比調用構造函數和析構函數的總開銷小,則第一種效率高,否則第二種的效率高。

  避免過大的循環:

代碼1:

for(inti = 0; i < n; ++i)
{
  fun1();
  fun2();
}

代碼2:


for(inti = 0; i < n; ++i)
{
  fun1();
}
for(inti = 0; i < n; ++i)
{
  fun2();
}

  這就要看fun1和fun2這兩個函數的規模(或複雜性)了,如果這多個函數的代碼語句很少,則代碼1的運行效率高一些,但是若fun1和fun2的語句有很多,規模較大,則代碼2的運行效率會比代碼1顯著高得多。

  如果fun1和fun2的代碼量很大,例如都大於Cache的容量,則在代碼1中,就不能充分利用Cache了(由時間局部性和空間局部性可知),因爲每循環一次,都要把Cache中的內容踢出,重新從內存中加載另一個函數的代碼指令和數據,而代碼2則更很好地利用了Cache,利用兩個循環語句,每個循環所用到的數據幾乎都已加載到Cache中,每次循環都可從Cache中讀寫數據,訪問內存較少,速度較快,理論上來說只需要完全踢出fun1的數據1次即可。

  基本上不會在自己的主循環裏搞什麼運算工作,絕對是先計算好了,再到循環裏查表;

  對於一些不需要循環變量參加運算的任務可以把它們放到循環外面,這裏的任務包括表達式、函數的調用、指針運算、數組訪問等,應該將沒有必要執行多次的操作全部集合在一起,放到一個init的初始化程序中進行。

延時函數 :

通常使用的延時函數均採用自加的形式:
    void delay (void)
    {
unsigned int i;
    for (i=0;i<1000;i++) ;
    }
將其改爲自減延時函數:
    void delay (void)
    {
unsigned int i;
        for (i=1000;i>0;i--) ;
    }
//兩個函數的延時效果相似,但幾乎所有的C編譯對後一種函數生成的代碼均比前一種代碼少
//1~3個字節,因爲幾乎所有的MCU均有爲0轉移的指令,採用後一種方式能夠生成這類指令。

  使用do…while循環編譯後生成的代碼的長度短於while循環;
  把相關循環放到一個循環裏,也會加快速度。

switch:

  Switch語句中根據發生頻率來進行case排序,Switch 可能轉化成多種不同算法的代碼。其中最常見的是跳轉表和比較鏈/樹。當switch用比較鏈的方式轉化時,編譯器會產生if-else-if的嵌套代碼,並按照順序進行比較,匹配時就跳轉到滿足條件的語句執行。所以可以對case的值依照發生的可能性進行排序,把最有可能的放在第一位,這樣可以提高性能。此外,在case中推薦使用小的連續的整數,因爲在這種情況下,所有的編譯器都可以把switch 轉化成跳轉表。

  將大的switch語句轉爲嵌套switch語句 ;

if esle:

  要提升循環的性能,減少多餘的常量計算非常有用(比如,不隨循環變化的計算)。不好的代碼(在for()中包含不變的if()):

for( i... )
{
  if( CONSTANT0 )
  {
    DoWork0( i )// 假設這裏不改變CONSTANT0的值
  }
  else
  {
    DoWork1( i )// 假設這裏不改變CONSTANT0的值
  }
}
推薦的代碼:
if( CONSTANT0 )
{
  for( i...)
  {
    DoWork0( i );
  }
}
else
{
  for( i...)
  {
    DoWork1( i );
  }
} 

  無限循環:for (;;)指令少,不佔用寄存器,而且沒有判斷、跳轉,比while (1)好。


三、局部變量VS靜態變量

  局部變量存在於堆棧中最大的好處是,函數能重複使用內存,當一個函數調用完畢時,退出程序堆棧,內存空間被回收,當新的函數被調用時,局部變量又可以重新使用相同的地址。當一塊數據被反覆讀寫,其數據會留在CPU的一級緩存(Cache)中,訪問速度非常快。而靜態變量卻不存在於堆棧中。可以說靜態變量是低效的。


四、避免使用多重繼承

  在C++中,支持多繼承,即一個子類可以有多個父類。書上都會跟我們說,多重繼承的複雜性和使用的困難,並告誡我們不要輕易使用多重繼承。其實多重繼承並不僅僅使程序和代碼變得更加複雜,還會影響程序的運行效率。

  這是因爲在C++中每個對象都有一個this指針指向對象本身,而C++中類對成員變量的使用是通過this的地址加偏移量來計算的,而在多重繼承的情況下,這個計算會變量更加複雜,從而降低程序的運行效率。而爲了解決二義性,而使用虛基類的多重繼承對效率的影響更爲嚴重,因爲其繼承關係更加複雜和成員變量所屬的父類關係更加複雜。


五、函數優化

(1)將小粒度函數聲明爲內聯函數(inline):

  內聯函數不是在調用時發生控制轉移,而是在編譯時將函數體嵌入在每一個調用處。編譯時,類似宏替換,使用函數體替換調用處的函數名。一般在代碼中用inline修飾,但是能否形成內聯函數,需要看編譯器對該函數定義的具體處理。

  調用函數是需要保護現場,爲局部變量分配內存,函數結束後還要恢復現場等開銷,而內聯函數則是把它的代碼直接寫到調用函數處,所以不需要這些開銷,但會使程序的源代碼長度變大。
  所以若是小粒度的函數,如下面的Max函數,由於不需要調用普通函數的開銷,所以可以提高程序的效率。

int Max(inta, intb)
{
  returna>b?a:b;
}

(2)不定義不使用的返回值

  函數定義並不知道函數返回值是否被使用,假如返回值從來不會被用到,應該使用void來明確聲明函數不返回任何值。

(3)減少函數調用參數

  使用全局變量比函數傳遞參數更加有效率。這樣做去除了函數調用參數入棧和函數完成後參數出棧所需要的時間。然而決定使用全局變量會影響程序的模塊化和重入,故要慎重使用。

(4)所有函數都應該有原型定義

  一般來說,所有函數都應該有原型定義。原型定義可以傳達給編譯器更多的可能用於優化的信息。

(5)儘可能使用常量(const)

  儘可能使用常量(const)。C++ 標準規定,如果一個const聲明的對象的地址不被獲取,允許編譯器不對它分配儲存空間。這樣可以使代碼更有效率,而且可以生成更好的代碼。

(6)把本地函數聲明爲靜態的(static)

  如果一個函數只在實現它的文件中被使用,把它聲明爲靜態的(static)以強制使用內部連接。否則,默認的情況下會把函數定義爲外部連接。這樣可能會影響某些編譯器的優化——比如,自動內聯。


六、指針和數組索引

  用指針運算代替數組索引,這樣做常常能產生又快又短的代碼。與數組索引相比,指針一般能使代碼速度更快,佔用空間更少。使用多維數組時差異更明顯。下面的代碼作用是相同的,但是效率不一樣。

 for(;;){                
    A= array[t++];
 } 

p=arrayfor(;;){
     a= *(p++); 
 }

  指針方法的優點是,array的地址每次裝入地址p後,在每次循環中只需對p增量操作。在數組索引方法中,每次循環中都必須根據t值求數組下標的複雜運算。


七、使用盡量小的數據類型

  能夠使用字符型(char)定義的變量,就不要使用整型(int)變量來定義;能夠使用整型變量定義的變量就不要用長整型(long int),能不使用浮點型(float)變量就不要使用浮點型變量。當然,在定義變量後不要超過變量的作用範圍,如果超過變量的範圍賦值,C編譯器並不報錯,但程序運行結果卻錯了,而且這樣的錯誤很難發現。

  在ICCAVR中,可以在Options中設定使用printf參數,儘量使用基本型參數(%c、%d、%x、%X、%u和%s格式說明符),少用長整型參數(%ld、%lu、%lx和%lX格式說明符),至於浮點型的參數(%f)則儘量不要使用,其它C編譯器也一樣。在其它條件不變的情況下,使用%f參數,會使生成的代碼的數量增加很多,執行速度降低。


八,運算

  求餘:位操作只需一個指令週期即可完成,而大部分的C編譯器的“%”運算均是調用子程序來完成,代碼長、執行速度慢。通常,只要求是求2n方的餘數,均可使用位操作的方法來代替。

    a=a%8;
可以改爲:
    a=a&7;

  乘除法:既使是在沒有內置硬件乘法器的AVR單片機中,乘法運算的子程序比平方運算的子程序代碼短,執行速度快。用移位的方法得到代碼比調用乘除法子程序生成的代碼效率高。實際上,只要是乘以或除以一個整數,均可以用移位的方法得到結果;

    a=pow(a, 2);
可以改爲:
    a=a*a;

移位:

    a=a*9
可以改爲:
    a=(a<<3)+a

  複合賦值表達式(如a-=1及a+=1等)都能夠生成高質量的程序代碼。


九,成員佈局

(1)按數據類型的長度排序 ;把結構體的成員按照它們的類型長度排序,聲明成員時把長的類型放在短的前面。編譯器要求把長型數據類型存放在偶數地址邊界。在申明一個複雜的數據類型 (既有多字節數據又有單字節數據) 時,應該首先存放多字節數據,然後再存放單字節數據,這樣可以避免內存的空洞。

(2)把結構體填充成最長類型長度的整倍數 :把結構體填充成最長類型長度的整倍數。照這樣,如果結構體的第一個成員對齊了,所有整個結構體自然也就對齊了。

(3)把頻繁使用的指針型參數拷貝到本地變量 ;避免在函數中頻繁使用指針型參數指向的值。因爲編譯器不知道指針之間是否存在衝突,所以指針型參數往往不能被編譯器優化。這樣數據不能被存放在寄存器中,而且明顯地佔用了內存帶寬;用把指針型參數保存到本地變量。否則,請在函數一開始把指針指向的數據保存到本地變量。如果需要的話,在函數結束前拷貝回去。

void isqrt(unsigned long a, unsigned long* q, unsigned long* r)
{
  unsigned long qq, rr; //用兩個變量;
  qq = a;
  if (a > 0)
  {
    while (qq > (rr = a / qq))
    {
      qq = (qq + rr) >> 1;
    }
  }
  rr = a - qq * qq;
  *q = qq;
  *r = rr;
}


十,register變量

  在聲明局部變量的時候可以使用register關鍵字。這就使得編譯器把變量放入一個多用途的寄存器中,而不是在堆棧中,合理使用這種方法可以提高執行速度。函數調用越是頻繁,越是可能提高代碼的速度。

  在最內層循環避免使用全局變量和靜態變量,除非你能確定它在循環週期中不會動態變化,大多數編譯器優化變量都只有一個辦法,就是將他們置成寄存器變量,而對於動態變量,它們乾脆放棄對整個表達式的優化。儘量避免把一個變量地址傳遞給另一個函數,雖然這個還很常用。C語言的編譯器們總是先假定每一個函數的變量都是內部變量,這是由它的機制決定的,在這種情況下,它們的優化完成得最好。但是,一旦一個變量有可能被別的函數改變,這幫兄弟就再也不敢把變量放到寄存器裏了,嚴重影響速度。看例子:

a = b();
c(&d);

  因爲d的地址被c函數使用,有可能被改變,編譯器不敢把它長時間的放在寄存器裏,一旦運行到c(&d),編譯器就把它放回內存,如果在循環裏,會造成N次頻繁的在內存和寄存器之間讀寫d的動作;

1、register修飾符暗示編譯程序相應的變量將被頻繁地使用,如果可能的話,應將其保存在CPU的寄存器中,以加快其存儲速度。例如下面的內存塊拷貝代碼,

/* Procedure for the assignment of structures, */
/* if the C compiler doesn't support this feature */
  #ifdef NOSTRUCTASSIGN
  memcpy (d, s, l)
{
    register char *d;
  register char *s;
  register int i;
  while (i--)
  *d++ = *s++;
  }
#endif

但是使用register修飾符有幾點限制:

(1)register變量必須是能被CPU所接受的類型。
這通常意味着register變量必須是一個單個的值,並且長度應該小於或者等於整型的長度。不過,有些機器的寄存器也能存放浮點數。
(2)因爲register變量可能不存放在內存中,所以不能用“&”來獲取register變量的地址。
(3)只有局部自動變量和形式參數可以作爲寄存器變量,其它(如全局變量)不行。
在調用一個函數時佔用一些寄存器以存放寄存器變量的值,函數調用結束後釋放寄存器。此後,在調用另外一個函數時又可以利用這些寄存器來存放該函數的寄存器變量。
(4)局部靜態變量不能定義爲寄存器變量。不能寫成:register static int a, b, c;
(5)由於寄存器的數量有限(不同的cpu寄存器數目不一),不能定義任意多個寄存器變量,而且某些寄存器只能接受特定類型的數據(如指針和浮點數),因此真正起作用的register修飾符的數目和類型都依賴於運行程序的機器,而任何多餘的register修飾符都將被編譯程序所忽略。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章