Global Sources
電子工程專輯
 
電子工程專輯 > 嵌入式技術
 
 
嵌入式技術  

針對嵌入式SoC應用最佳化C語言編程

上網時間: 2006年10月12日     打印版  Bookmark and Share  字型大小:  

關鍵字:SoC  structured C  Tensilica  Xtensa  可配置處理器 

開發執行在SoC內的嵌入式處理器核心程式時,通常有兩個主要目的,即讓處理器執行頻率降到最低;以及使記憶體開銷降到最小。這兩項因素的重要性會因不同的計劃而異,而以下兩項關鍵將大幅影響設計團隊滿足這些目標的能力,即開發原始程式的編譯器以最佳化程式碼的效率;以及用於開發原始程式碼的編程風格。本文將深入討論這兩種因素,並提出一些製作小型且快速之C程式的建議。

編譯器通常由前端和後端兩部份組成。前端通常是指語法和語義的處理過程,後端通常是指最佳化、程式碼產生,以及針對特定處理器的最佳化過程。很多好的編譯器後端依賴於多層的中間表述(IR)。最佳化和程式碼產生從高層(類別輸入程式的句法)到底層逐級地傳遞中間表述。與處理器無關的最佳化一般傾向於在編譯過程早期於較高IR層上實現,而針對特定處理器的最佳化一般傾向於在編譯過程的後期在底層IR上來實現。資訊透過不同IR層向下傳遞,這樣底層最佳化可以充分利用編譯器早期處理得到的高層資訊。

Tensilica針對其Xtensa可配置處理器和Diamond標準處理器的XCC/C++編譯器包含四個基本的最佳化級,從-O0到-O3,對應著不斷提高的最佳化等級。表1描述了這些等級及其相對應的程式碼大小和內部過程分析(IPA)。通常情況下,XCC編譯器一次最佳化一個文件,但是它也可以執行內部過程分析(透過加入IPA的編譯選項)。當在多個原文件上最佳化整個應用程式時,最佳化將會被延遲到鏈接的步驟之後進行。表2描述了目前編譯器(包括XCC編譯器)支援的最佳化內容部份列表。

XCC編譯器還可以利用編譯產生的性能分析數據。性能分析的反饋可以幫助編譯器減輕分支跳轉的延遲。另外,反饋可以讓編譯器只是插入那些最常用的函數(inline),並妥善處理常用程式碼段中暫存器溢出的問題。因此,性能分析反饋允許XCC編譯器在所有地方進行正常最佳化的同時,還可以透過最佳化應用中的臨界部份進行加速。

C語言編碼建議規則

為利用編譯器獲得最佳性能,程式設計師必須像編譯器一樣思考問題,並瞭解C語言和目標處理器之間的關係。以下一些基本原則可協助所有嵌入式程式設計師在不需很大努力的情況下獲得性能更好的編譯程式碼。

1. 觀察編譯後的程式碼

完全瞭解編譯器對全部程式碼如何編譯是不可能的。如果XCC編譯器設置了─S或者-save-temps編譯選項,編譯將產生匯編輸出,同時還有一些為了加強瞭解而添加的註釋。對於那些性能要求很高的程式碼,你可以觀察編譯結果是否符合你的期望。如果不是,請考慮以下規則。

2. 瞭解混淆發生的情況

C語言允許任意使用指針,這增加了混淆出現的機會,這允許程式用很多種方法去引用同一數據對象。如果全局變量的地址被作為子程式的參數傳遞,這個變量可以透過它的名字或透過指針被引用。這就是一種混淆,編譯器必須保守地把這樣的數據對象保存在記憶體中而不是暫存器中,並仔細地保持程式碼中可能引起混淆的變量的存取順序。可考慮下面的程式碼:

void foo(int *a, int *b)

{

     int i;

          for (i=0; i<100; i++) {

          *a += b[i];

          }

}

您會設想編譯器應該產生程式碼並在循環開始前將*a保存到一個暫存器中,同時在循環中把b[i]保存到一個暫存器裡面然後將它加到*a所在的暫存器裡。但事實上卻是,編譯器產生的結果是*a被放置在記憶體裡面,因為a和b可以產生混淆情況,*a也許是b數組的一個元素。雖然看起來在這個例子中不太可能出現這種混淆,但是編譯器是沒法確定這種情況是否會發生的。有幾個技巧可以針對混淆情況協助編譯器實現更好的編譯工作:你可以使用IPA編譯選項進行編譯,你可以用全局變量代替參數,你可以使用特殊編譯選項進行編譯,或在聲明變量中使用_restrict屬性。

3. 指針常常引起混淆

編譯器識別指針指向的目標對象經常會遇到問題。程式設計師可透過使用本地變量幫助編譯器避免混淆,具體方法是使用本地變量儲存依據指針存取獲得的值,因為不直接的作業和調用會影響指針引用的值而非本地變量的值。因此,編譯器會把本地變量放到暫存器中。

以下例子顯示如何正確使用指針以避免混淆並產生更好的編譯程式碼。在這個例子中,最佳化者不知道*p++=0是否會修改len,所以它不能把len放到暫存器內獲得性能提升。相反地,在每個循環中,len都被放到了記憶體內。

int len = 10;

void

zero(char *p)

{

     int i;

     for (i=0; i}

透過使用本地變量而非全局變量,可以避免混淆。

int len = 10;

void

zero(char *p)

{

     int local_len = len;

     int i;

     for (i=0; i< local_len; i++) *p++ = 0;

}

4. const和restrict限定詞

_restrict限定詞告訴編譯器可以假設有資格的指針是唯一存取某記憶體或數據對象的方式。透過這個指針的Load和Store作業,將不會引起與這個函數內部其它Load和Store作業的混淆,除非透過這個指針存取。例如:

float x[ARRAY_SIZE];

float *c = x;

void f4_opt(int n, float * __restrict a, float * __restrict b)

{

     int i;

     /* No data dependence across iterations because of __restrict */

     for (i = 0; i < n; i++)

     a[i] = b[i] + c[i];

}

5. 使用本地變量

這是因為全局變量會在整個程式的生命週期裡面保留數值。編譯器必須認為全局變量可能透過指針被存取。可考慮下列程式碼:

int g;

void foo()

{

     int i;

     for (i=0; i<100; i++){

         fred(i,g);

     }

}

理想情況下,g在每次fred循環時被加載一次,且其值將被傳遞到一個暫存器內給fred函數使用。但編譯器不知道fred是否會修改g的值。如果fred不會修改g的值,你應該像下面一樣使用本地變量。這樣做可以避免每次調用fred函數時加載g到一個暫存器裡面。

int g;

void foo()

{

     int i, local_g=g;

     for (i=0; i<100; i++){

         fred(i,local_g);

     }

}

6. 使用正確的數據類型

C程式設計師對於數據類型一般都會有他們習慣上的假設,但是編譯器卻需要很謹慎地對待這些假設。例如,在幾乎所有現代的電腦架構上,一個unsigned char使用8位元表示從0到255。一個C程式會假設對值為255的unsigned char加1會使其變為0。而實際上,現代32位元處理器不會執行上述的8位元加法,而是進行32位元數值加法。因此,如果一個unsigned char的本地變量進行加法,編譯器必須使用多條指令進行運算以保證加法後的符號擴展。因此,針對各種變量尤其是循環索引的變量,應盡量多的在可以的地方使用int型變量。

另外,許多嵌入式處理器有16位元乘法指令,而缺少32位元乘法指令。在這種情況下,32位元乘法將被仿效執行,一般情況下都是很慢的。如果數據被執行乘法作業並且運算結果不會超過16位元的精密度,那麼就使用short或者unsigned short變量。

7. 不要用不直接的調用

這是透過包含傳遞參數的函數指針的調用,因為那會產生不可預知的邊際效應(如修改全局變量),使最佳化難以進行。

8. 編寫返回數值的函數

9. 傳遞變量時使用數值而不是指針或者全局變量

傳遞大結構的數據時才使用指針。每個透過數值被傳遞的結構都應該在函數調用入口處被完全拷貝儲存過。

10. 使用變量地址

因為本地變量的地址會引起混淆,降低程式性能,與全局變量一樣。

11. 用const聲明指針參數

如果函數體內不會修改到指針指向的對象,就要用const聲明指針參數,這樣可以讓編譯器避免不必要的反面假設。

12. 使用數組而不是指針,考慮透過指針存取數組的程式碼:

for (i=0; i<100; i++)

     *p++ = ...

在每次循環中,*p被賦值。這種對指針對象的賦值會阻礙最佳化。某些情況下,指針指向它自己,那麼這種賦值就會修改指針本身的值,這就會強迫編譯器每次循環都重新加載該指針。還有,編譯器不能確定這個指針不會被循環體以外所使用,所以每次循環外都要依據增量的數值更新該指針。因此,最好使用下面的程式碼:

for (i=0; i<100; i++)

     p[i] = ...

13. 編寫簡單易懂的程式碼

編譯器擅長製作複雜的最佳化,如函數嵌入和在適當的時候循環體展開。但編譯器不擅長簡化程式碼,他們不會合併循環或者不用函數嵌入。在原始程式中為了支援某些處理器架構進行的手工循環體展開會降低程式的可移植性,因為這阻止了編譯器自動為其他處理器架構進行正確的循環體展開和函數嵌入。

14. 避免編寫參數數量可變的函數

如果一定要這麼做,使用ANSI標準方法:stdarg.h.。使用數據表替代if-then-else或者switch分支處理。如考慮以下程式碼:

typedef enum { BLUE, GREEN, RED, NCOLORS } COLOR;

替代

switch ( c ) {

     case CASE0: x = 5; break;

     case CASE1: x = 10; break;

     case CASE2: x = 1; break;

}

使用

static int Mapping[NCOLORS] = { 5, 10, 1 };

...

x = Mapping[c];

15. 依靠libc函數庫(比如:strcpy、strlen、strcmp、bcopy、bzero、memset和memcpy)。這些函數是經過精心最佳化的。

本文小結

編譯器設計者已經開發了很多複雜的最佳化功能以使最新的處理器獲得最大性能,他們還在繼續開發更智慧的最佳化演算法。應用程式開發人員可以透過使用恰當的編程規則來盡可能多地利用編譯器的這些最佳化功能。


表1:一些XCC C/C++編譯器最佳化開關


表2:某些現代編譯器的最佳化方法

作者:Dror Maydan

軟體工程總監

Steve Leibson

技術專家

Tensilica公司




投票數:   加入我的最愛
我來評論 - 針對嵌入式SoC應用最佳化C語言編程
評論:  
*  您還能輸入[0]個字
*驗證碼:
 
論壇熱門主題 熱門下載
 •   將邁入40歲的你...存款多少了  •  深入電容觸控技術就從這個問題開始
 •  我有一個數位電源的專利...  •  磷酸鋰鐵電池一問
 •   關於設備商公司的工程師(廠商)薪資前景  •  計算諧振轉換器的同步整流MOSFET功耗損失
 •   Touch sensor & MEMS controller  •  針對智慧電表PLC通訊應用的線路驅動器
 •   下週 深圳 llC 2012 關於PCB免費工具的研討會  •  邏輯閘的應用


EE人生人氣排行
 
返回頁首