針對(duì)嵌入式SoC應(yīng)用的C編程優(yōu)化
開(kāi)發(fā)運(yùn)行在SoC內(nèi)的嵌入式處理器內(nèi)核的程序時(shí),工程師有兩個(gè)主要目的:運(yùn)行得足夠快,使處理器運(yùn)行的頻率降到最低;消耗盡量少的內(nèi)存,使內(nèi)存開(kāi)銷(xiāo)降到最小。
對(duì)于不同的項(xiàng)目,有時(shí)候這兩個(gè)因素的重要性會(huì)不一樣。下面兩個(gè)關(guān)鍵因素極大地影響著設(shè)計(jì)團(tuán)隊(duì)滿足這些目標(biāo)的能力:開(kāi)發(fā)源程序的編譯器對(duì)代碼的優(yōu)化效率以及用于開(kāi)發(fā)源代碼的編程風(fēng)格。本文將深入地討論這兩種因素,并提出一些創(chuàng)建小而快的C程序的建議。
編譯器原理
編譯器通常是由前端和后端兩部分組成。前端通常是指語(yǔ)法和語(yǔ)義的處理過(guò)程,后端通常是指優(yōu)化、代碼生成,以及針對(duì)特定處理器的優(yōu)化過(guò)程。很多好的編譯器后端依賴于多層的中間表述(IR)。優(yōu)化和代碼生成從高層(類(lèi)型輸入程序的句法)到低層逐級(jí)地傳遞中間表述。與處理器無(wú)關(guān)的優(yōu)化一般傾向于在編譯過(guò)程的早期在較高IR層上實(shí)現(xiàn),而針對(duì)特定處理器的優(yōu)化一般傾向于在編譯過(guò)程的后期在低層IR上來(lái)實(shí)現(xiàn)。信息通過(guò)不同IR層向下傳遞,這樣低層優(yōu)化可以充分利用編譯器早期處理得到的高層信息。
Tensilica針對(duì)其Xtensa可配置處理器和Diamond標(biāo)準(zhǔn)處理器的XCC/C++編譯器包含四個(gè)基本的優(yōu)化級(jí),從-O0到-O3,對(duì)應(yīng)著不斷提高的優(yōu)化級(jí)別。表1描述了這些級(jí)別及其相對(duì)應(yīng)的代碼大小和內(nèi)部過(guò)程分析(IPA)。缺省情況下,XCC編譯器一次優(yōu)化一個(gè)文件,但是它也可以執(zhí)行內(nèi)部過(guò)程分析(通過(guò)加入IPA的編譯選項(xiàng))。當(dāng)在多個(gè)原文件上優(yōu)化整個(gè)應(yīng)用程序時(shí),優(yōu)化將會(huì)被延遲到鏈接的步驟之后進(jìn)行。表2描述了當(dāng)前編譯器(包括 XCC編譯器)支持的優(yōu)化內(nèi)容部分列表。
XCC編譯器還可以利用編譯產(chǎn)生的性能分析數(shù)據(jù)。性能分析的反饋可以幫助編譯器減輕分支跳轉(zhuǎn)的延遲。另外,反饋可以讓編譯器只是插入那些最常用的函數(shù)(inline),并且妥善處理常用代碼段中寄存器溢出的問(wèn)題。因此,性能分析反饋允許XCC編譯器在所有地方進(jìn)行正常優(yōu)化的同時(shí),還可以通過(guò)優(yōu)化應(yīng)用中的臨界部分進(jìn)行加速。
一些有用的C編碼規(guī)則
為了利用編譯器得到最好的性能,編程人員需要像編譯器一樣思考問(wèn)題,并且理解C語(yǔ)言和目標(biāo)處理器之間的關(guān)系。下面的一些基本原則可以幫助所有嵌入式編程人員在不需很大努力的情況下獲得性能好很多的編譯代碼。
1. 觀察編譯得到的代碼
完全理解編譯器對(duì)全部代碼如何編譯是不可能的。如果XCC編譯器設(shè)置了—S或者-save-temps編譯選項(xiàng),編譯將產(chǎn)生匯編輸出并且還有一些為了理解而添加的注釋。對(duì)于那些性能要求很高的代碼,你可以觀察編譯結(jié)果是否符合你的期望。如果不是,請(qǐng)考慮以下規(guī)則。
2. 了解混淆發(fā)生的情況
C語(yǔ)言允許任意地使用指針,這增加了混淆出現(xiàn)的機(jī)會(huì),這允許程序用很多種方法去引用同一數(shù)據(jù)對(duì)象。如果全局變量的地址被作為子程序的參數(shù)傳遞,這個(gè)變量可以通過(guò)它的名字或者通過(guò)指針被引用。這就是一種混淆,編譯器必須保守地把這樣的數(shù)據(jù)對(duì)象保存在內(nèi)存中而不是寄存器中,并且仔細(xì)地保持代碼中可能引起混淆的變量的訪問(wèn)順序??紤]下面的代碼:
void foo(int *a, int *b)
{
int i;
for (i=0; i<100; i++) {
*a += b[i];
}
}
您會(huì)設(shè)想編譯器應(yīng)該產(chǎn)生代碼是在循環(huán)開(kāi)始前將*a保存到一個(gè)寄存器里面,并且在循環(huán)中把b[i]保存到一個(gè)寄存器里面然后將它加到*a所在的寄存器里。但事實(shí)上卻是,編譯器產(chǎn)生的結(jié)果是*a被放置在內(nèi)存里面,因?yàn)閍和b可以產(chǎn)生混淆情況,*a也許是b數(shù)組的一個(gè)元素。雖然看起來(lái)在這個(gè)例子中不太可能出現(xiàn)這種混淆,但是編譯器是沒(méi)法確定這種情況是否會(huì)發(fā)生的。有幾個(gè)技巧可以針對(duì)混淆的情況,幫助編譯器能做到更好的編譯工作:你可以使用-IPA 編譯選項(xiàng)進(jìn)行編譯,你可以用全局變量代替參數(shù),你可以使用特殊編譯選項(xiàng)進(jìn)行編譯,或者可以在聲明變量中使用_restrict屬性。
3. 指針常常引起混淆
編譯器識(shí)別指針指向的目標(biāo)對(duì)象經(jīng)常會(huì)遇到問(wèn)題。程序員可以通過(guò)使用本地變量幫助編譯器去避免混淆,具體方法是使用本地變量去存儲(chǔ)依據(jù)指針訪問(wèn)獲得的值,因?yàn)椴恢苯拥牟僮骱驼{(diào)用影響指針引用的值而不是本地變量的值。因此,編譯器會(huì)把本地變量放到寄存器里面去。
下面的例子顯示如何正確使用指針以避免混淆從而產(chǎn)生更好的編譯代碼。在這個(gè)例子里面,優(yōu)化者不知道*p++=0是否會(huì)修改len,所以它不能把len放到寄存器里面去獲得性能提升。相反每個(gè)循環(huán)中,len都被放到了內(nèi)存里面。
int len = 10;
void
zero(char *p)
{
int i;
for (i=0; i
}
通過(guò)使用本地變量而不是全局變量,可以避免混淆。
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限定詞告訴編譯器可以假設(shè)有資格的指針是唯一訪問(wèn)某內(nèi)存或數(shù)據(jù)對(duì)象的方式。通過(guò)這個(gè)指針的Load和Store操作不會(huì)引起與這個(gè)函數(shù)內(nèi)部其它Load和Store操作的混淆,除非通過(guò)這個(gè)指針的訪問(wèn)。例如:
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. 使用本地變量替代全局變量
這是因?yàn)槿肿兞繒?huì)在整個(gè)程序的生命周期里面保留數(shù)值。編譯器必須認(rèn)為全局變量可能通過(guò)指針被訪問(wèn)??紤]下面的代碼:
int g;
void foo()
{
int i;
for (i=0; i<100; i++){
fred(i,g);
}
}
理想情況下,g在每次fred循環(huán)時(shí)被加載一次,并且它的值將被傳遞到一個(gè)寄存器里面給fred函數(shù)使用。但是,編譯器不知道fred是否會(huì)修改g 的值。如果fred不會(huì)修改g的值,你應(yīng)該像下面一樣,使用本地變量。這樣做可以避免每次調(diào)用fred函數(shù)時(shí)加載g到一個(gè)寄存器里面。
int g;
void foo()
{
int i, local_g=g;
for (i=0; i<100; i++){
fred(i,local_g);
}
}
6. 針對(duì)數(shù)據(jù)結(jié)構(gòu)使用正確的數(shù)據(jù)類(lèi)型
C編程人員對(duì)于數(shù)據(jù)類(lèi)型一般都會(huì)有他們習(xí)慣上的假設(shè),但是編譯器卻需要很謹(jǐn)慎地對(duì)待這些假設(shè)。比如,在幾乎所有現(xiàn)代的計(jì)算機(jī)架構(gòu)上,一個(gè) unsigned char使用8位表示從0到255。一個(gè)C程序會(huì)假設(shè)對(duì)值為255的unsigned char加1會(huì)使其變?yōu)?。而實(shí)際上,現(xiàn)代32位處理器是不會(huì)執(zhí)行上述的那種8位加法,而是進(jìn)行32位數(shù)值的加法。因此,如果一個(gè)unsigned char的本地變量進(jìn)行加法,編譯器必須使用多條指令進(jìn)行運(yùn)算以保證加法后的符號(hào)擴(kuò)展。因此,針對(duì)各種變量尤其是循環(huán)索引的變量,應(yīng)該盡量多的在可以的地方使用int型變量。
另外,許多嵌入式處理器有16位乘法指令,而缺少32位乘法指令。在這種情況下,32位乘法將被仿效執(zhí)行,一般情況下都是很慢的。如果數(shù)據(jù)被執(zhí)行乘法操作并且計(jì)算結(jié)果不會(huì)超過(guò)16位的精度,那么就使用short或者unsigned short變量。
7. 不要用不直接的調(diào)用
這是通過(guò)包含傳遞參數(shù)的函數(shù)指針的調(diào)用,因?yàn)槟菚?huì)產(chǎn)生不可預(yù)知的邊際效應(yīng)(比如修改全局變量),使得優(yōu)化難以進(jìn)行。
8. 編寫(xiě)返回?cái)?shù)值的函數(shù)而不是返回指針的函數(shù)
9. 傳遞變量時(shí)使用數(shù)值而不是指針或者全局變量
傳遞大結(jié)構(gòu)的數(shù)據(jù)時(shí),才使用指針。每個(gè)通過(guò)數(shù)值被傳遞的結(jié)構(gòu)都應(yīng)該在函數(shù)調(diào)用入口處被完全拷貝存儲(chǔ)過(guò)。
10. 使用變量的地址會(huì)使程序性能降低
因?yàn)楸镜刈兞康牡刂窌?huì)引起混淆,這如同全局變量一樣。
11. 用const聲明指針參數(shù)
如果函數(shù)體內(nèi)不會(huì)修改到指針指向的對(duì)象,就要用const聲明指針參數(shù),這樣可以讓編譯器避免不必要的反面假設(shè)。
12. 使用數(shù)組而不是指針,考慮下面通過(guò)指針訪問(wèn)數(shù)組的代碼
for (i=0; i<100; i++)
*p++ = ...
在每次循環(huán)中,*p被賦值。這種對(duì)指針對(duì)象的賦值會(huì)阻礙優(yōu)化。某些情況下,指針指向它自己,那么這種賦值就會(huì)修改指針本身的值,這就會(huì)強(qiáng)迫編譯器每次循環(huán)都重新加載該指針。還有,編譯器不能確定這個(gè)指針不會(huì)被循環(huán)體以外的使用,所以每次循環(huán)外都要依據(jù)增量的數(shù)值更新該指針。因此,最好使用下面的代碼:
for (i=0; i<100; i++)
p[i] = ...
13. 編寫(xiě)簡(jiǎn)單易懂的代碼
編譯器擅長(zhǎng)創(chuàng)建復(fù)雜的優(yōu)化,比如函數(shù)嵌入和在適當(dāng)?shù)臅r(shí)候循環(huán)體展開(kāi)。但編譯器不擅長(zhǎng)簡(jiǎn)化代碼,他們不會(huì)合并循環(huán)或者不用函數(shù)嵌入。在源程序中為了支持某些處理器架構(gòu)進(jìn)行的手工的循環(huán)體展開(kāi)會(huì)降低程序的可移植性,因?yàn)檫@阻止了編譯器自動(dòng)為其他的處理器架構(gòu)進(jìn)行正確的循環(huán)體展開(kāi)和函數(shù)嵌入。
14. 避免編寫(xiě)參數(shù)數(shù)量可變的函數(shù)
如果一定要這么做,使用ANSI標(biāo)準(zhǔn)方法:stdarg.h.。使用數(shù)據(jù)表替代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函數(shù)庫(kù)(比如:strcpy、strlen、strcmp、bcopy、bzero、memset和memcpy)。這些函數(shù)是經(jīng)過(guò)精心優(yōu)化的。
表1:一些XCC C/C++編譯器優(yōu)化開(kāi)關(guān)
本文小結(jié)
編譯器設(shè)計(jì)者已經(jīng)開(kāi)發(fā)了很多復(fù)雜的優(yōu)化功能以使最新的處理器獲得最大的性能,并且他們還在繼續(xù)開(kāi)發(fā)更智能的優(yōu)化算法。應(yīng)用程序開(kāi)發(fā)人員可以通過(guò)使用恰當(dāng)?shù)?strong>編程規(guī)則來(lái)盡可能多地利用編譯器的這些優(yōu)化功能。