關于一些C語言代碼優(yōu)化的方法,我慷慨解囊了大家酌情收藏
關于一些C語言代碼優(yōu)化的方法,我慷慨解囊了大家酌情收藏
簡介
在最近的一個項目中,我們需要開發(fā)一個運行在移動設備上但不保證圖像高質量的輕量級JPEG庫。期間,我總結了一些讓程序運行更快的方法。在本篇文章中,我收集了一些經驗和方法。
應用這些經驗和方法,可以幫助我們從執(zhí)行速度和內存使用等方面來優(yōu)化C語言代碼。
///插播一條:我自己在今年年初錄制了一套還比較系統的入門單片機教程,想要的同學找我拿就行了免費的,私信我就可以哦~點我頭像黑色字體加我地球呺也能領取哦。最近比較閑,帶做畢設,帶學生參加省級或以上比賽///
正文開始:
盡管在C代碼優(yōu)化方面有很多的指南,但是關于編譯和你使用的編程機器方面的優(yōu)化知識卻很少。
通常,為了讓你的程序運行的更快,程序的代碼量可能需要增加。代碼量的增加又可能會對程序的復雜度和可讀性帶來不利的影響。這對于在手機、PDA等對于內存使用有很多限制的小型設備上編寫程序時是不被允許的。
因此,在代碼優(yōu)化時,我們的座右銘應該是確保內存使用和執(zhí)行速度兩方面都得到優(yōu)化。
聲明
實際上,在我的項目中,我使用了很多優(yōu)化ARM編程的方法(該項目是基于ARM平臺的),也使用了很多互聯網上面的方法。但并不是所有文章提到的方法都能起到很好的作用。所以,我對有用的和高效的方法進行了總結收集。同時,我還修改了其中的一些方法,使他們適用于所有的編程環(huán)境,而不是局限于ARM環(huán)境。
哪里需要使用這些方法?
沒有這一點,所有的討論都無從談起。程序優(yōu)化最重要的就是找出待優(yōu)化的地方,也就是找出程序的哪些部分或者哪些模塊運行緩慢亦或消耗大量的內存。只有程序的各部分經過了優(yōu)化,程序才能執(zhí)行的更快。
程序中運行最多的部分,特別是那些被程序內部循環(huán)重復調用的方法最該被優(yōu)化。
對于一個有經驗的碼農,發(fā)現程序中最需要被優(yōu)化的部分往往很簡單。此外,還有很多工具可以幫助我們找出需要優(yōu)化的部分。我使用過Visual C++內置的性能工具profiler來找出程序中消耗最多內存的地方。
另一個我使用過的工具是英特爾的Vtune,它也能很好的檢測出程序中運行最慢的部分。根據我的經驗,內部或嵌套循環(huán),調用第三方庫的方法通常是導致程序運行緩慢的最主要的起因。
整形數
如果我們確定整數非負,就應該使用unsigned int而不是int。有些處理器處理無符號unsigned整形數的效率遠遠高于有符號signed整形數(這是一種很好的做法,也有利于代碼具體類型的自解釋)。
因此,在一個緊密循環(huán)中,聲明一個int整形變量的最好方法是:
registerunsignedint variable_name;
記住,整形in的運算速度高浮點型float,并且可以被處理器直接完成運算,而不需要借助于FPU(浮點運算單元)或者浮點型運算庫。
盡管這不保證編譯器一定會使用到寄存器存儲變量,也不能保證處理器處理能更高效處理unsigned整型,但這對于所有的編譯器是通用的。
例如在一個計算包中,如果需要結果精確到小數點后兩位,我們可以將其乘以100,然后盡可能晚的把它轉換為浮點型數字。
除法和取余數
在標準處理器中,對于分子和分母,一個32位的除法需要使用20至140次循環(huán)操作。除法函數消耗的時間包括一個常量時間加上每一位除法消耗的時間。
Time (numerator / denominator) = C0 + C1* log2 (numerator / denominator)
= C0 + C1 * (log2 (numerator) - log2 (denominator)).
對于ARM處理器,這個版本需要20+4.3N次循環(huán)。這是一個消耗很大的操作,應該盡可能的避免執(zhí)行。有時,可以通過乘法表達式來替代除法。
例如,假如我們知道b是正數并且b*c是個整數,那么(a/b)>c可以改寫為a>(c * b)。如果確定操作數是無符號unsigned的,使用無符號unsigned除法更好一些,因為它比有符號signed除法效率高。
合并除法和取余數
在一些場景中,同時需要除法(x/y)和取余數(x%y)操作。這種情況下,編譯器可以通過調用一次除法操作返回除法的結果和余數。如果既需要除法的結果又需要余數,我們可以將它們寫在一起,如下所示:
intfunc_div_and_mod(int a, int b)
{
return (a / b) + (a % b);
}
通過2的冪次進行除法和取余數
如果除法中的除數是2的冪次,我們可以更好的優(yōu)化除法。編譯器使用移位操作來執(zhí)行除法。因此,我們需要盡可能的設置除數為2的冪次(例如64而不是66)。并且依然記住,無符號unsigned整數除法執(zhí)行效率高于有符號signed整形出發(fā)。
typedefunsignedint uint;
uint div32u(uint a)
{
return a / 32;
}
intdiv32s(int a)
{
return a / 32;
}
上面兩種除法都避免直接調用除法函數,并且無符號unsigned的除法使用更少的計算機指令。由于需要移位到0和負數,有符號signed的除法需要更多的時間執(zhí)行。
取模的一種替代方法
我們使用取余數操作符來提供算數取模。但有時可以結合使用if語句進行取模操作??紤]如下兩個例子:
uint modulo_func1(uint count)
{
return (++count % 60);
}
uint modulo_func2(uint count)
{
if (++count >= 60)
count = 0;
return (count);
}
優(yōu)先使用if語句,而不是取余數運算符,因為if語句的執(zhí)行速度更快。這里注意新版本函數只有在我們知道輸入的count結余0至59時在能正確的工作。
使用數組下標
如果你想給一個變量設置一個代表某種意思的字符值,你可能會這樣做:
switch ( queue )
{
case0 : letter = 'W';
break;
case1 : letter = 'S';
break;
case2 : letter = 'U';
break;
}
或者這樣做:
if ( queue == 0 )
letter = 'W';
elseif ( queue == 1 )
letter = 'S';
else letter = 'U';
一種更簡潔、更快的方法是使用數組下標獲取字符數組的值。如下:
staticchar *classes="WSU";
letter = classes[queue];
全局變量
全局變量絕不會位于寄存器中。使用指針或者函數調用,可以直接修改全局變量的值。因此,編譯器不能將全局變量的值緩存在寄存器中,但這在使用全局變量時便需要額外的(常常是不必要的)讀取和存儲。所以,在重要的循環(huán)中我們不建議使用全局變量。
如果函數過多的使用全局變量,比較好的做法是拷貝全局變量的值到局部變量,這樣它才可以存放在寄存器。這種方法僅僅適用于全局變量不會被我們調用的任意函數使用。例子如下:
intf(void);
intg(void);
int errs;
voidtest1(void)
{
errs += f();
errs += g();
}
voidtest2(void)
{
int localerrs = errs;
localerrs += f();
localerrs += g();
errs = localerrs;
}
注意,test1必須在每次增加操作時加載并存儲全局變量errs的值,而test2存儲localerrs于寄存器并且只需要一個計算機指令。
使用別名
考慮如下的例子:
voidfunc1(int *data )
{
int i;
for(i=0; i<10; i++)
{
anyfunc( *data, i);
}
}
盡管*data的值可能從未被改變,但編譯器并不知道anyfunc函數不會修改它,所以程序必須在每次使用它的時候從內存中讀取它。如果我們知道變量的值不會被改變,那么就應該使用如下的編碼:
voidfunc1(int *data )
{
int i;
int localdata;
localdata = *data;
for(i=0; i<10; i++)
{
anyfunc (localdata, i);
}
}
這為編譯器優(yōu)化代碼提供了條件。
變量的生命周期分割
由于處理器中寄存器是固定長度的,程序中數字型變量在寄存器中的存儲是有一定限制的。
有些編譯器支持“生命周期分割”(live-range splitting),也就是說在程序的不同部分,變量可以被分配到不同的寄存器或者內存中。
變量的生命周期開始于對它進行的最后一次賦值,結束于下次賦值前的最后一次使用。在生命周期內,變量的值是有效的,也就是說變量是活著的。不同生命周期之間,變量的值是不被需要的,也就是說變量是死掉的。
這樣,寄存器就可以被其余變量使用,從而允許編譯器分配更多的變量使用寄存器。
需要使用寄存器分配的變量數目需要超過函數中不同變量生命周期的個數。如果不同變量生命周期的個數超過了寄存器的數目,那么一些變量必須臨時存儲于內存。這個過程就稱之為分割。
編譯器首先分割最近使用的變量,用以降低分割帶來的消耗。禁止變量生命周期分割的方法如下:
·限定變量的使用數量:這個可以通過保持函數中的表達式簡單、小巧、不使用太多的變量實現。將較大的函數拆分為小而簡單的函數也會達到很好的效果。
·對經常使用到的變量采用寄存器存儲:這樣允許我們告訴編譯器該變量是需要經常使用的,所以需要優(yōu)先存儲于寄存器中。然而,在某種情況下,這樣的變量依然可能會被分割出寄存器。
變量類型
C編譯器支持基本類型:char、short、int、long(包括有符號signed和無符號unsigned)、float和double。使用正確的變量類型至關重要,因為這可以減少代碼和數據的大小并大幅增加程序的性能。
局部變量
我們應該盡可能的不使用char和short類型的局部變量。對于char和short類型,編譯器需要在每次賦值的時候將局部變量減少到8或者16位。這對于有符號變量稱之為有符號擴展,對于無符號變量稱之為零擴展。
這些擴展可以通過寄存器左移24或者16位,然后根據有無符號標志右移相同的位數實現,這會消耗兩次計算機指令操作(無符號char類型的零擴展僅需要消耗一次計算機指令)。
可以通過使用int和unsigned int類型的局部變量來避免這樣的移位操作。這對于先加載數據到局部變量,然后處理局部變量數據值這樣的操作非常重要。無論輸入輸出數據是8位或者16位,將它們考慮為32位是值得的。
考慮下面的三個函數:
intwordinc(int a)
{
return a + 1;
}
shortshortinc(short a)
{
return a + 1;
}
charcharinc(char a)
{
return a + 1;
}
盡管結果均相同,但是第一個程序片段運行速度高于后兩者。
指針
我們應該盡可能的使用引用值的方式傳遞結構數據,也就是說使用指針,否則傳遞的數據會被拷貝到棧中,從而降低程序的性能。我曾見過一個程序采用傳值的方式傳遞非常大的結構數據,然后這可以通過一個簡單的指針更好的完成。
函數通過參數接受結構數據的指針,如果我們確定不改變數據的值,我們需要將指針指向的內容定義為常量。例如:
voidprint_data_of_a_structure(const Thestruct *data_pointer)
{
...printf contents of the structure...
}
這個示例告訴編譯器函數不會改變外部參數的值(使用const修飾),并且不用在每次訪問時都進行讀取。同時,確保編譯器限制任何對只讀結構的修改操作從而給予結構數據額外的保護。
指針鏈
指針鏈經常被用于訪問結構數據。例如,常用的代碼如下:
typedefstruct { int x, y, z; } Point3;
typedefstruct { Point3 *pos, *direction; } Object;
voidInitPos1(Object *p)
{
p->pos->x = 0;
p->pos->y = 0;
p->pos->z = 0;
}
然而,這種的代碼在每次操作時必須重復調用p->pos,因為編譯器不知道p->pos->x與p->pos是相同的。一種更好的方法是緩存p->pos到一個局部變量:
voidInitPos2(Object *p)
{
Point3 *pos = p->pos;
pos->x = 0;
pos->y = 0;
pos->z = 0;
}
另一種方法是在Object結構中直接包含Point3類型的數據,這能完全消除對Point3使用指針操作。
條件執(zhí)行
條件執(zhí)行語句大多在if語句中使用,也在使用關系運算符(<,==,>等)或者布爾值表達式(&&,!等)計算復雜表達式時使用。對于包含函數調用的代碼片段,由于函數返回值會被銷毀,因此條件執(zhí)行是無效的。
因此,保持if和else語句盡可能簡單是十分有益處的,因為這樣編譯器可以集中處理它們。關系表達式應該寫在一起。
下面的例子展示編譯器如何使用條件執(zhí)行:
intg(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;
}
由于條件被聚集到一起,編譯器能夠將他們集中處理。
布爾表達式和范圍檢查
一個常用的布爾表達式是用于判斷變量是否位于某個范圍內,例如,檢查一個圖形坐標是否位于一個窗口內:
boolPointInRectangelArea(Point p, Rectangle *r)
{
return (p.x >= r->xmin && p.x xmax &&
p.y >= r->ymin && p.y ymax);
}
這里有一種更快的方法:x>min && x<>x可以轉換為(unsigned)(x-min))。這對于min等于0時更為有益。優(yōu)化后的代碼如下:
boolPointInRectangelArea(Point p, Rectangle *r)
{
return ((unsigned) (p.x - r->xmin) xmax &&
(unsigned) (p.y - r->ymin) 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代碼中每次關系運算符的調用,編譯器都會發(fā)出一個比較指令。如果操作符是上面提到的,編譯器便會優(yōu)化掉比較指令。例如:
intaFunction(int x, int y)
{
if (x + y 0)
return1;
else
return0;
}
盡可能的使用上面的判斷方式,這可以在關鍵循環(huán)中減少比較指令的調用,進而減少代碼體積并提高代碼性能。C語言沒有借位和溢出位的概念,因此,如果不借助匯編,不可能直接使用借位標志C和溢出位標志V。但編譯器支持借位(無符號溢出),例如:
intsum(int x, int y)
{
int res;
res = x + y;
if ((unsigned) res unsigned) x) // carry set? //
res++;
return res;
}
懶檢測開發(fā)
在if(a>10 && b=4)這樣的語句中,確保AND表達式的第一部分最可能較快的給出結果(或者最早、最快計算),這樣第二部分便有可能不需要執(zhí)行。
用switch()函數替代if…else…
對于涉及if…else…else…這樣的多條件判斷,例如:
if( val == 1)
dostuff1();
elseif (val == 2)
dostuff2();
elseif (val == 3)
dostuff3();
使用switch可能更快:
switch( val )
{
case1: dostuff1(); break;
case2: dostuff2(); break;
case3: dostuff3(); break;
}
在if()語句中,如果最后一條語句命中,之前的條件都需要被測試執(zhí)行一次。Switch允許我們不做額外的測試。如果必須使用if…else…語句,將最可能執(zhí)行的放在最前面。
二分中斷
使用二分方式中斷代碼而不是讓代碼堆成一列,不要像下面這樣做:
if(a==1) {
} elseif(a==2) {
} elseif(a==3) {
} elseif(a==4) {
} elseif(a==5) {
} elseif(a==6) {
} elseif(a==7) {
} elseif(a==8)
{
}
使用下面的二分方式替代它,如下:
if(a4) {
if(a==1) {
} elseif(a==2) {
} elseif(a==3) {
} elseif(a==4) {
}
}
else
{
if(a==5) {
} elseif(a==6) {
} elseif(a==7) {
} elseif(a==8) {
}
}
或者如下:
if(a4)
{
if(a2)
{
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(a6)
{
if(a==5)
{
/* a is 5 */
}
else
{
/* a must be 6 */
}
}
else
{
if(a==7)
{
/* a is 7 */
}
else
{
/* a must be 8 */
}
}
}
比較如下兩種case語句:
其他技巧
通常,可以使用空間換時間。如果你能緩存經常用的數據而不是重新計算,這便能更快的訪問。比如sine和cosine查找表,或者偽隨機數。
·盡量不在循環(huán)中使用++和–。例如:while(n–){},這有時難于優(yōu)化。
·減少全局變量的使用。
·除非像聲明為全局變量,使用static修飾變量為文件內訪問。
·盡可能使用一個字大小的變量(int、long等),使用它們(而不是char,short,double,位域等)機器可能運行的更快。
·不使用遞歸。遞歸可能優(yōu)雅而簡單,但需要太多的函數調用。
·不在循環(huán)中使用sqrt開平方函數,計算平方根非常消耗性能。
·一維數組比多維數組更快。
·編譯器可以在一個文件中進行優(yōu)化-避免將相關的函數拆分到不同的文件中,如果將它們放在一起,編譯器可以更好的處理它們(例如可以使用inline)。
·單精度函數比雙精度更快。
·浮點乘法運算比浮點除法運算更快-使用val*0.5而不是val/2.0。
·加法操作比乘法快-使用val+val+val而不是val*3。
·put()函數比printf()快,但不靈活。
·使用#define宏取代常用的小函數。
·二進制/未格式化的文件訪問比格式化的文件訪問更快,因為程序不需要在人為可讀的ASCII和機器可讀的二進制之間轉化。如果你不需要閱讀文件的內容,將它保存為二進制。
·如果你的庫支持mallopt()函數(用于控制malloc),盡量使用它。MAXFAST的設置,對于調用很多次malloc工作的函數由很大的性能提升。如果一個結構一秒鐘內需要多次創(chuàng)建并銷毀,試著設置mallopt選項。
最后,但是是最重要的是-將編譯器優(yōu)化選項打開!看上去很顯而易見,但卻經常在產品推出時被忘記。編譯器能夠在更底層上對代碼進行優(yōu)化,并針對目標處理器執(zhí)行特定的優(yōu)化處理。