DSP程式最佳化方法

來源:互聯網
上載者:User
1、選擇合適的演算法和資料結構 選擇一種合適的資料結構很重要,如果在一堆隨機存放的數中使用了大量的插入和刪除指令,那使用鏈表要快得多。數組與指標語句具有十分密切的關係,一般來說,指標比較靈活簡潔,而數組則比較直觀,容易理解。對於大部分的編譯器,使用指標比使用數組產生的程式碼更短,執行效率更高。 在許多種情況下,可以用指標運算代替數組索引,這樣做常常能產生又快又短的代碼。與數組索引相比,指標一般能使代碼速度更快,佔用空間更少。使用多維陣列時差異更明顯。下面的代碼作用是相同的,但是效率不一樣。     數組索引                 指標運算     For(;;){                 p=array     A=array[t++];           for(;;){                                 a=*(p++);      。。。。。。。。。                   。。。。。。     }                       } 指標方法的優點是, array 的地址每次裝入地址 p 後,在每次迴圈中只需對 p 增量操作。在數組索引方法中,每次迴圈中都必鬚根據 t 值求數組下標的複雜運算。 2、使用盡量小的資料類型 能夠使用字元型 (char) 定義的變數,就不要使用整型 (int) 變數來定義;能夠使用整型變數定義的變數就不要用長整型 (long int) ,能不使用浮點型 (float) 變數就不要使用浮點型變數。當然,在定義變數後不要超過變數的作用範圍,如果超過變數的範圍賦值, C 編譯器並不報錯,但程式運行結果卻錯了,而且這樣的錯誤很難發現。 在 ICCAVR 中,可以在 Options 中設定使用 printf 參數,盡量使用基本型參數 (%c 、 %d 、 %x 、 %X 、 %u 和 %s 格式說明符 ) ,少用長整型參數 (%ld 、 %lu 、 %lx 和 %lX 格式說明符 ) ,至於浮點型的參數 (%f) 則盡量不要使用,其它 C 編譯器也一樣。在其它條件不變的情況下,使用 %f 參數,會使產生的程式碼的數量增加很多,執行速度降低。 3、減少運算的強度 ( 1)、查表(遊戲程式員必修課) 一個聰明的遊戲大蝦,基本上不會在自己的主迴圈裡搞什麼運算工作,絕對是先計算好了,再到迴圈裡查表。看下面的例子: 舊代碼:     long factorial(int i)     {         if (i == 0)             return 1;         else             return i * factorial(i - 1);     } 新代碼:     static long factorial_table[] =         {1 , 1 , 2 , 6 , 24 , 120 , 720   /* etc */ };     long factorial(int i)     {         return factorial_table[i];     } 如果表很大,不好寫,就寫一個 init 函數,在迴圈外臨時產生表格。 ( 2)、求餘運算     a=a%8; 可以改為:     a=a&7; 說明:位操作只需一個指令周期即可完成,而大部分的 C 編譯器的“ % ”運算均是調用子程式來完成,代碼長、執行速度慢。通常, 只要求是求 2n 方的餘數,均可使用位操作的方法來代替。 ( 3)、平方運算     a=pow(a, 2.0); 可以改為:     a=a*a; 說明:在有內建硬體乘法器的單片機中 ( 如 51 系列 ) ,乘法運算比求平方運算快得多,因為浮點數的求平方是通過調用子程式來實現的,在內建硬體乘法器的 AVR 單片機中,如 ATMega163 中,乘法運算只需 2 個刻度就可以完成。既使是在沒有內建硬體乘法器的 AVR 單片機中,乘法運算的子程式比平方運算的子程式碼短,執行速度快。 如果是求 3 次方,如:     a=pow(a , 3 。 0); 更改為:     a=a*a*a ; 則效率的改善更明顯。 ( 4)、用移位實現乘除法運算     a=a*4;     b=b/4; 可以改為:     a=a<<2;     b=b>>2; 通常如果需要乘以或除以 2n ,都可以用移位的方法代替。在 ICCAVR 中,如果乘以 2n ,都可以產生左移的代碼,而乘以其它的整數或除以任何數,均調用乘除法子程式。用移位的方法得到代碼比調用乘除法子程式產生的程式碼效率高。實際上,只要是乘以或除以一個整數,均可以用移位的方法得到結果,如:     a=a*9 可以改為: a=(a<<3)+a 採用運算量更小的運算式替換原來的運算式,下面是一個經典例子 : 舊代碼 :     x = w % 8;     y = pow(x , 2.0);     z = y * 33;     for (i = 0;i < MAX;i++)     {         h = 14 * i;         printf("%d" , h);     } 新代碼 :     x = w & 7;         /* 位操作比求餘運算快 */     y = x * x;            /* 乘法比平方運算快 */     z = (y << 5) + y;               /* 位移乘法比乘法快 */     for (i = h = 0; i < MAX; i++)     {         h += 14;                   /* 加法比乘法快 */         printf("%d" , h); } ( 5)、避免不必要的整數除法   整數除法是整數運算中最慢的,所以應該儘可能避免。一種可能減少整數除法的地方是連除,這裡除法可以由乘法代替。這個替換的副作用是有可能在算乘積時會溢出,所以只能在一定範圍的除法中使用。             不好的代碼: int i , j , k , m ; m = i / j / k ;              推薦的代碼: int i , j , k , m ; m = i / (j * k) ; ( 6)、使用增量和減量操作符     在使用到加一和減一操作時盡量使用增量和減量操作符,因為增量符語句比指派陳述式更快,原因在於對大多數 CPU 來說,對記憶體字的增、減量操作不必明顯地使用取記憶體和寫記憶體的指令,比如下面這條語句: x=x+1; 模仿大多數微機組合語言為例,產生的代碼類似於:     move A , x       ; 把 x 從記憶體取出存入累加器 A     add A , 1         ; 累加器 A 加 1 store x           ; 把新值存回 x 如果使用增量操作符,產生的程式碼如下:     incr x            ;x 加 1 顯然,不用取指令和存指令,增、減量操作執行的速度加快,同時間長度度也縮短了。 ( 7)、使用複合賦值運算式 複合賦值運算式 ( 如 a-=1 及 a+=1 等 ) 都能夠產生高品質的程式碼。 ( 8)、提取公用的子運算式 在某些情況下, C++ 編譯器不能從浮點運算式中提出公用的子運算式,因為這意味著相當於對錶達式重新排序。需要特別指出的是,編譯器在提取公用子運算式前不能按照代數的等價關係重新安排運算式。這時,程式員要手動地提出公用的子運算式(在 VC.NET 裡有一項“全域最佳化”選項可以完成此工作,但效果就不得而知了)。 不好的代碼: float a , b , c , d , e , f ; 。。。 e = b * c / d ; f = b / d * a ; 推薦的代碼: float a , b , c , d , e , f ; 。。。 const float t(b / d) ; e = c * t ; f = a * t ;    不好的代碼: float a , b , c , e , f ; 。。。 e = a / c ; f = b / c ; 推薦的代碼: float a , b , c , e , f ; 。。。 const float t(1.0f / c) ; e = a * t ; f = b * t ; 4、結構體成員的布局   很多編譯器有“使結構體字,雙字或四字對齊”的選項。但是,還是需要改善結構體成員的對齊,有些編譯器可能分配給結構體成員空間的順序與他們聲明的不同。但是,有些編譯器並不提供這些功能,或者效果不好。所以,要在付出最少代價的情況下實現最好的結構體和結構體成員對齊,建議採取下列方法: ( 1)按資料類型的長度排序 把結構體的成員按照它們的類型長度排序,聲明成員時把長的類型放在短的前面。編譯器要求把長型資料類型存放在 偶數地址邊界 。在申明一個複雜的資料類型 ( 既有多位元組資料又有單位元組資料 ) 時,應該首先存放多位元組資料,然後再存放單位元組資料,這樣可以避免記憶體的空洞。編譯器自動地把結構的執行個體對齊在記憶體的偶數邊界。 ( 2)把結構體填充成最長類型長度的整倍數 把結構體填充成最長類型長度的整倍數。照這樣,如果結構體的第一個成員對齊了,所有整個結構體自然也就對齊了。下面的例子示範了如何對結構體成員進行重新排序: 不好的代碼,普通順序: struct {                             char a[5] ;        long k ;   double x ; } baz ;    推薦的代碼,新的順序並手動填充了幾個位元組: struct {                             double x ;                             long k ;                             char a[5] ; char pad[7] ; } baz ; 這個規則同樣適用於類的成員的布局。 ( 3)按資料類型的長度排序本地變數 當編譯器分配給本地變數空間時,它們的順序和它們在原始碼中聲明的順序一樣,和上一條規則一樣,應該把長的變數放在短的變數前面。如果第一個變數對齊了,其它變數就會連續的存放,而且不用填充位元組自然就會對齊。有些編譯器在分配變數時不會自動改變變數順序,有些編譯器不能產生 4 位元組對齊的棧,所以 4 位元組可能不對齊。下面這個例子示範了本地變數聲明的重新排序:               不好的代碼,普通順序 short ga , gu , gi ; long foo , bar ; double x , y , z[3] ; char a , b ; float baz ; 推薦的代碼,改進的順序 double z[3] ; double x , y ; long foo , bar ; float baz ; short ga , gu , gi ;    ( 4)把頻繁使用的指標型參數拷貝到本地變數 避免在函數中頻繁使用指標型參數指向的值。因為編譯器不知道指標之間是否存在衝突,所以指標型參數往往不能被編譯器最佳化。這樣資料不能被存放在寄存器中,而且明顯地佔用了記憶體頻寬。注意,很多編譯器有“假設不衝突”最佳化開關(在 VC 裡必須手動添加編譯器命令列 /Oa 或 /Ow ),這允許編譯器假設兩個不同的指標總是有不同的內容,這樣就不用把指標型參數儲存到本地變數。否則,請在函數一開始把指標指向的資料儲存到本地變數。如果需要的話,在函數結束前拷貝回去。 不好的代碼: // 假設 q != r void isqrt(unsigned long a , unsigned long* q , unsigned long* r) {    *q = a ;    if (a > 0)    {      while (*q > (*r = a / *q))      {        *q = (*q + *r) >> 1 ;      }    }    *r = a - *q * *q ; }    推薦的代碼: // 假設 q != r 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 ; } 5、迴圈最佳化 ( 1)、充分分解小的迴圈    要充分利用 CPU的指令緩衝,就要充分分解小的迴圈。 特別是當迴圈體本身很小的時候,分解迴圈可以提高效能。注意 :很多編譯器並不能自動分解迴圈。 不好的代碼: // 3D 轉化:把向量 V 和 4x4 矩陣 M 相乘 for (i = 0 ; i < 4 ; i ++) {    r[i] = 0 ;    for (j = 0 ; j < 4 ; j ++)    {      r[i] += M[j][i]*V[j] ;    } } 推薦的代碼: r[0] = M[0][0]*V[0] + M[1][0]*V[1] + M[2][0]*V[2] + M[3][0]*V[3] ; r[1] = M[0][1]*V[0] + M[1][1]*V[1] + M[2][1]*V[2] + M[3][1]*V[3] ; r[2] = M[0][2]*V[0] + M[1][2]*V[1] + M[2][2]*V[2] + M[3][2]*V[3] ; r[3] = M[0][3]*V[0] + M[1][3]*V[1] + M[2][3]*V[2] + M[3][3]*v[3] ; ( 2)、提取公用部分 對於一些不需要迴圈變數參加運算的任務可以把它們放到迴圈外面,這裡的任務包括運算式、函數的調用、指標運算、數組訪問等,應該將沒有必要執行多次的操作全部集合在一起,放到一個 init 的初始化程式中進行。 ( 3)、延時函數 通常使用的延時函數均採用自加的形式:     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 轉移的指令 ,採用後一種方式能夠產生這類指令。在使用 while 迴圈時也一樣,使用自減指令控制迴圈會比使用自加指令控制迴圈產生的程式碼更少 1~3 個字母。但是在迴圈中有通過迴圈變數“ i ”讀寫數組的指令時,使用預減迴圈有可能使數組超界,要引起注意。 ( 4)、while迴圈和do…while迴圈 用 while 迴圈時有以下兩種迴圈形式: unsigned int i;     i=0;     while (i<1000)     {         i++;            // 使用者程式     } 或: unsigned int i;     i=1000; do {           i--;           // 使用者程式 } while (i>0); 在這兩種迴圈中, 使用 do … while 迴圈編譯後產生的程式碼的長度短於 while 迴圈。 ( 6)、迴圈展開 這是經典的速度最佳化,但許多編譯器 ( 如 gcc -funroll-loops) 能自動完成這個事,所以現在你自己來最佳化這個顯得效果不明顯。 舊代碼 : for (i = 0; i < 100; i++) { do_stuff(i); } 新代碼 : for (i = 0; i < 10; ) { do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; do_stuff(i); i++; } 可以看出, 新代碼裡比較指令由 100 次降低為 10 次,迴圈時間節約了 90% 。 不過注意 : 對於中間變數或結果被更改的迴圈,編譯器往往拒絕展開, ( 怕擔責任唄 ) ,這時候就需要你自己來做展開工作了。 還有一點請注意,在有內部指令 cache 的 CPU 上 ( 如 MMX 晶片 ) ,因為迴圈展開的代碼很大,往往 cache 溢出,這時展開的代碼會頻繁地在 CPU 的 cache 和記憶體之間調來調去,又因為 cache 速度很高,所以此時迴圈展開反而會變慢。 還有就是迴圈展開會影響向量運算最佳化。 ( 6)、迴圈嵌套 把相關迴圈放到一個迴圈裡,也會加快速度。 舊代碼 : for (i = 0; i < MAX; i++)          /* initialize 2d array to 0's */     for (j = 0; j < MAX; j++)         a[i][j] = 0.0;     for (i = 0; i < MAX; i++) /* put 1's along the diagonal */         a[i][i] = 1.0;    新代碼 : for (i = 0; i < MAX; i++)          /* initialize 2d array to 0's */ {     for (j = 0; j < MAX; j++)         a[i][j] = 0.0;      a[i][i] = 1.0;                           /* put 1's along the diagonal */ } ( 7)、Switch語句中根據發生頻率來進行case排序 Switch 可能轉化成多種不同演算法的代碼。其中最常見的是 跳轉表 和 比較鏈 / 樹 。當 switch 用比較鏈的方式轉化時,編譯器會產生 if-else-if 的嵌套代碼,並按照順序進行比較,匹配時就跳轉到滿足條件的語句執行。所以可以對 case 的值依照發生的可能性進行排序,把最有可能的放在第一位,這樣可以提高效能。此外,在 case 中推薦使用小的連續的整數,因為在這種情況下,所有的編譯器都可以把 switch 轉化成跳轉表。 不好的代碼: int days_in_month , short_months , normal_months , long_months ; 。。。。。。 switch (days_in_month) {    case 28:    case 29:      short_months ++ ;      break ;    case 30:      normal_months ++ ;      break ;    case 31:      long_months ++ ;      break ;    default:      cout << "month has fewer than 28 or more than 31 days" << endl ;      break ; }    推薦的代碼: int days_in_month , short_months , normal_months , long_months ; 。。。。。。 switch (days_in_month) {    case 31:      long_months ++ ;      break ;    case 30:      normal_months ++ ;      break ;    case 28:    case 29:      short_months ++ ;      break ;    default:      cout << "month has fewer than 28 or more than 31 days" << endl ;      break ; }      ( 8)、將大的switch語句轉為嵌套switch語句 當 switch 語句中的 case 標號很多時,為了減少比較的次數,明智的做法是把大 switch 語句轉為嵌套 switch 語句。把發生頻率高的 case 標號放在一個 switch 語句中,並且是嵌套 switch 語句的最外層,發生相對頻率相對低的 case 標號放在另一個 switch 語句中。比如,下面的程式段把相對發生頻率低的情況放在預設的 case 標號內。 pMsg=ReceiveMessage();         switch (pMsg->type)         {         case FREQUENT_MSG1:         handleFrequentMsg();         break;         case FREQUENT_MSG2:         handleFrequentMsg2();         break;         。。。。。。         case FREQUENT_MSGn:         handleFrequentMsgn();         break;         default:                      // 嵌套部分用來處理不經常發生的訊息         switch (pMsg->type)         {         case INFREQUENT_MSG1:         handleInfrequentMsg1();         break;         case INFREQUENT_MSG2:         handleInfrequentMsg2();         break;         。。。。。。         case INFREQUENT_MSGm:         handleInfrequentMsgm();         break;         }         } 如果 switch 中每一種情況下都有很多的工作要做,那麼把整個 switch 語句用一個 指向函數指標的表 來替換會更加有效,比如下面的 switch 語句,有三種情況:     enum MsgType{Msg1 , Msg2 , Msg3}         switch (ReceiveMessage()         {         case Msg1;         。。。。。。          case Msg2;         。。。。。         case Msg3;         。。。。。         } 為了提高執行速度,用下面這段代碼來替換這個上面的 switch 語句。         /* 準備工作 */         int handleMsg1(void);         int handleMsg2(void);         int handleMsg3(void);         /* 建立一個函數指標數組 */         int (*MsgFunction [])()={handleMsg1 , handleMsg2 , handleMsg3};         /* 用下面這行更有效代碼來替換 switch 語句 */         status=MsgFunction[ReceiveMessage()](); ( 9)、迴圈轉置 有些機器對 JNZ( 為 0 轉移 ) 有特別的指令處理,速度非常快,如果你的迴圈對方向不敏感,可以由大向小迴圈。 舊代碼 :                for (i = 1; i <= MAX; i++)               {                              。。。                            } 新代碼 :                i = MAX+1;              while (--i)                           {                            。。。                           } 不過千萬注意,如果指標操作使用了 i 值,這種方法可能引起指標越界的嚴重錯誤 (i = MAX+1;) 。當然你可以通過對 i 做加減運算來糾正,但是這樣就起不到加速的作用,除非類似於以下情況: 舊代碼 :     char a[MAX+5];     for (i = 1; i <= MAX; i++)     {          *(a+i+4)=0;     } 新代碼 :     i = MAX+1;     while (--i)     {         *(a+i+4)=0; } ( 10)、公用代碼塊 一些公用處理模組,為了滿足各種不同的調用需要,往往在內部採用了大量的 if-then-else 結構,這樣很不好,判斷語句如果太複雜,會消耗大量的時間的,應該盡量減少公用代碼塊的使用。 ( 任何情況下,空間最佳化和時間最佳化都是對立的 -- 東樓 ) 。當然,如果僅僅是一個 (3==x) 之類的簡單判斷,適當使用一下,也還是允許的。記住,最佳化永遠是追求一種平衡,而不是走極端。 ( 11)提升迴圈的效能 要提升迴圈的效能,減少多餘的常量計算非常有用(比如,不隨迴圈變化的計算)。 不好的代碼 ( 在 for() 中包含不變的 if()) : for( i 。。。 ) {    if( CONSTANT0 )    {      DoWork0( i ) ; // 假設這裡不改變 CONSTANT0 的值    }    else    {      DoWork1( i ) ; // 假設這裡不改變 CONSTANT0 的值    } } 推薦的代碼: if( CONSTANT0 ) {    for( i 。。。 )    {      DoWork0( i ) ;    } } else {    for( i 。。。 )    {      DoWork1( i ) ;    } }    如果已經知道 if() 的值,這樣可以避免重複計算。雖然不好的代碼中的分支可以簡單地預測,但是由於推薦的代碼在進入迴圈前分支已經確定,就可以減少對分支預測的依賴。 ( 12)、選擇好的無限迴圈 在編程中,我們常常需要用到無限迴圈,常用的兩種方法是 while (1) 和 for ( ;; ) 。這兩種方法效果完全一樣,但那一種更好呢?然我們看看它們編譯後的代碼: 編譯前: while (1) ; 編譯後: mov eax , 1 test eax , eax je foo+23h jmp foo+18h    編譯前: for ( ;; ) ; 編譯後: jmp foo+23h 顯然, for ( ;; ) 指令少,不佔用寄存器,而且沒有判斷、跳轉,比 while (1) 好。 6、提高CPU的並行性 ( 1)使用並行代碼 儘可能把長的有依賴的代碼鏈分解成幾個可以在流水線執行單元中並存執行的沒有依賴的代碼鏈。很多進階語言,包括 C++ ,並不對產生的浮點運算式重新排序,因為那是一個相當複雜的過程。需要注意的是,重排序的代碼和原來的代碼在代碼上一致並不等價於計算結果一致,因為浮點操作缺乏精確度。在一些情況下,這些最佳化可能導致意料之外的結果。幸運的是,在大部分情況下,最後結果可能只有最不重要的位(即最低位)是錯誤的。 不好的代碼: double a[100] , sum ; int i ; sum = 0.0f ; for (i=0 ; i<100 ; i++) sum += a[i] ;    推薦的代碼: double a[100] , sum1 , sum2 , sum3 , sum4 , sum ; int i ; sum1 = sum2 = sum3 = sum4 = 0.0 ; for (i = 0 ; i < 100 ; i += 4) {    sum1 += a[i] ;    sum2 += a[i+1] ;    sum3 += a[i+2] ;    sum4 += a[i+3] ; } sum = (sum4+sum3)+(sum1+sum2) ;      要注意的是: 使用 4 路分解是因為這樣使用了 4 段流水線浮點加法,浮點加法的每一個段佔用一個刻度,保證了最大的資源使用率。 ( 2)避免沒有必要的讀寫依賴 當資料儲存到記憶體時存在讀寫依賴,即資料必須在正確寫入後才能再次讀取。雖然 AMD Athlon 等 CPU 有加速讀寫依賴延遲的硬體,允許在要儲存的資料被寫入記憶體前讀取出來,但是,如果避免了讀寫依賴並把資料儲存在內部寄存器中,速度會更快。在一段很長的又互相依賴的代碼鏈中,避免讀寫依賴顯得尤其重要。如果讀寫依賴發生在運算元組時,許多編譯器不能自動最佳化代碼以避免讀寫依賴。所以推薦程式員手動去消除讀寫依賴,舉例來說,引進一個可以儲存在寄存器中的臨時變數。這樣可以有很大的效能提升。下面一段代碼是一個例子: 不好的代碼: float x[VECLEN] , y[VECLEN] , z[VECLEN] ; 。。。。。。 for ( unsigned int k = 1 ; k < VECLEN ; k ++) {    x[k] = x[k-1] + y[k] ; } for (k = 1 ; k <VECLEN ; k++) {    x[k] = z[k] * (y[k] - x[k-1]) ; } 推薦的代碼: float x[VECLEN] , y[VECLEN] , z[VECLEN] ; 。。。。。。 float t(x[0]) ; for ( unsigned int k = 1 ; k < VECLEN ; k ++) {    t = t + y[k] ;    x[k] = t ; } t = x[0] ; for (k = 1 ; k < ; VECLEN ; k ++) {    t = z[k] * (y[k] - t) ;    x[k] = t ; }    7、迴圈不變計算 對於一些不需要迴圈變數參加運算的計算任務可以把它們放到迴圈外面,現在許多編譯器還是能自己幹這件事,不過對於中間使用了變數的算式它們就不敢動了,所以很多情況下你還得自己幹。對於那些在迴圈中調用的函數,凡是沒必要執行多次的操作通通提出來,放到一個 init 函數裡,迴圈前調用。另外盡量減少餵食次數,沒必要的話盡量不給它傳參,需要迴圈變數的話讓它自己建立一個靜態迴圈變數自己累加,速度會快一點。 還有就是結構體訪問,東樓的經驗,凡是在迴圈裡對一個結構體的兩個以上的元素執行了訪問,就有必要建立中間變數了 ( 結構這樣,那 C++ 的對象呢 ? 想想看 ) ,看下面的例子 : 舊代碼 :     total =     a->b->c[4]->aardvark +     a->b->c[4]->baboon +     a->b->c[4]->cheetah +     a->b->c[4]->dog; 新代碼 :     struct animals * temp = a->b->c[4];     total =     temp->aardvark +     temp->baboon +     temp->cheetah +     temp->dog; 一些老的 C 語言編譯器不做 彙總最佳化 ,而符合 ANSI 規範的新的編譯器可以自動完成這個最佳化,看例子 :     float a , b , c , d , f , g;     。。。     a = b / c * d;     f = b * g / c; 這種寫法當然要得,但是沒有最佳化     float a , b , c , d , f , g;     。。。     a = b / c * d;     f = b / c * g; 如果這麼寫的話,一個符合 ANSI 規範的新的編譯器可以只計算 b/c 一次,然後將結果代入第二個式子,節約了一次除法運算。 8、 函數最佳化   (1)Inline函數 在 C++ 中,關鍵字 Inline 可以被加入到任何函數的聲明中。 這個關鍵字請求編譯器用函數內部的代碼替換所有對於指出的函數的調用。 這樣做在兩個方面快於函數調用:第一,省去了調用指令需要的執行時間;第二,省去了傳遞變元和傳遞過程需要的時間。但是使用這種方法在最佳化程式速度的同時,程式長度變大了,因此需要更多的 ROM 。 使用這種最佳化在 Inline 函數頻繁調用並且只包含幾行代碼的時候是最有效。 ( 2)不定義不使用的傳回值 函數定義並不知道函數傳回值是否被使用,假如傳回值從來不會被用到,應該使用 void 來明確聲明函數不返回任何值。 ( 3)減少函數調用參數     使用 全域變數 比函數傳遞參數更加有效率。這樣做去除了函數調用參數入棧和函數完成後參數出棧所需要的時間。然而決定使用全域變數會影響程式的模組化和重入,故要謹慎使用。 ( 4)所有函數都應該有原型定義 一般來說,所有函數都應該有原型定義。原型定義可以傳達給編譯器更多的可能用於最佳化的資訊。 ( 5)儘可能使用常量(const) 儘可能使用常量 (const) 。 C++ 標準規定,如果一個 const 聲明的對象的地址不被擷取,允許編譯器不對它分配儲存空間。這樣可以使代碼更有效率,而且可以產生更好的代碼。 ( 6)把本地函式宣告為靜態(static)    如果一個函數只在實現它的檔案中被使用,把它聲明為靜態 (static)以強制使用內部串連。否則,預設的情況下會把函數定義為外部串連。這樣可能會影響某些編譯器的最佳化——比如,自動內聯。 9、採用遞迴 與 LISP之類的語言不同,C語言一開始就病態地喜歡用重複代碼迴圈,許多C程式員都是除非演算法要求,堅決不用遞迴。事實上,C編譯器們對最佳化遞迴調用一點都不反感,相反,它們還很喜歡幹這件事。只有在遞迴函式需要傳遞大量參數,可能造成瓶頸的時候,才應該使用迴圈代碼,其他時候,還是用遞迴好些。 10、變數 ( 1)register變數 在聲明局部變數的時候可以使用 register關鍵字。這就使得編譯器把變數放入一個多用途的寄存器中,而不是在堆棧中,合理使用這種方法可以提高執行速度。函數調用越是頻繁,越是可能提高代碼的速度。 在最內層迴圈避免使用全域變數和靜態變數,除非你能確定它在重複持續時間中不會動態變化,大多數編譯器最佳化變數都只有一個辦法,就是將他們置成寄存器變數,而對於動態變數,它們乾脆放棄對整個運算式的最佳化。盡量避免把一個變數地址傳遞給另一個函數,雖然這個還很常用。 C語言的編譯器們總是先假定每一個函數的變數都是內部變數,這是由它的機制決定的,在這種情況下,它們的最佳化完成得最好。但是,一旦一個變數有可能被別的函數改變,這幫兄弟就再也不敢把變數放到寄存器裡了,嚴重影響速度。看例子: a = b(); c(&d); 因為 d的地址被c函數使用,有可能被改變,編譯器不敢把它長時間的放在寄存器裡,一旦運行到c(&d),編譯器就把它放回記憶體,如果在迴圈裡,會造成N次頻繁的在記憶體和寄存器之間讀寫d的動作,眾所周知,CPU在系統匯流排上的讀寫速度慢得很。比如你的賽楊300,CPU主頻300,匯流排速度最多66M,為了一個匯流排讀,CPU可能要等4-5個周期,得。。得。。得。。想起來都打顫。 ( 2)、同時聲明多個變數優於單獨聲明變數 ( 3)、短變數名優於長變數名,應盡量使變數名短一點 ( 4)、在迴圈開始前聲明變數 11、使用嵌套的if結構 在 if結構中如果要判斷的並列條件較多,最好將它們拆分成多個if結構,然後嵌套在一起,這樣可以避免無謂的判斷。 說明: 上面的最佳化方案由王全明收集整理。很多資料來源與網上,出處不祥,在此對所有作者一併致謝! 該方案主要是考慮到在嵌入式開發中對程式執行速度的要求特別高,所以該方案主要是為了 最佳化程式的執行速度 。 注意:最佳化是有側重點的,最佳化是一門平衡的藝術,它往往要以犧牲程式的可讀性或者增加代碼長度為代價。 ( 任何情況下,空間最佳化和時間最佳化都是對立的 -- 東樓 )

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.