0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

從執(zhí)行速度和內(nèi)存使用等方面來(lái)優(yōu)化C語(yǔ)言代碼

Q4MP_gh_c472c21 ? 來(lái)源:嵌入式ARM ? 作者:嵌入式ARM ? 2020-11-16 16:55 ? 次閱讀

在本篇文章中,我收集了很多經(jīng)驗(yàn)和方法。應(yīng)用這些經(jīng)驗(yàn)和方法,可以幫助我們從執(zhí)行速度和內(nèi)存使用等方面來(lái)優(yōu)化C語(yǔ)言代碼。

簡(jiǎn)介

在最近的一個(gè)項(xiàng)目中,我們需要開發(fā)一個(gè)運(yùn)行在移動(dòng)設(shè)備上但不保證圖像高質(zhì)量的輕量級(jí)JPEG庫(kù)。期間,我總結(jié)了一些讓程序運(yùn)行更快的方法。

在本篇文章中,我收集了一些經(jīng)驗(yàn)和方法。應(yīng)用這些經(jīng)驗(yàn)和方法,可以幫助我們從執(zhí)行速度和內(nèi)存使用等方面來(lái)優(yōu)化C語(yǔ)言代碼。

盡管在C代碼優(yōu)化方面有很多的指南,但是關(guān)于編譯和你使用的編程機(jī)器方面的優(yōu)化知識(shí)卻很少。

通常,為了讓你的程序運(yùn)行的更快,程序的代碼量可能需要增加。代碼量的增加又可能會(huì)對(duì)程序的復(fù)雜度和可讀性帶來(lái)不利的影響。

這對(duì)于在手機(jī)、PDA等對(duì)于內(nèi)存使用有很多限制的小型設(shè)備上編寫程序時(shí)是不被允許的。因此,在代碼優(yōu)化時(shí),我們的座右銘應(yīng)該是確保內(nèi)存使用和執(zhí)行速度兩方面都得到優(yōu)化。

聲明

實(shí)際上,在我的項(xiàng)目中,我使用了很多優(yōu)化ARM編程的方法(該項(xiàng)目是基于ARM平臺(tái)的),也使用了很多互聯(lián)網(wǎng)上面的方法。但并不是所有文章提到的方法都能起到很好的作用。

所以,我對(duì)有用的和高效的方法進(jìn)行了總結(jié)收集。同時(shí),我還修改了其中的一些方法,使他們適用于所有的編程環(huán)境,而不是局限于ARM環(huán)境。

哪里需要使用這些方法?

沒有這一點(diǎn),所有的討論都無(wú)從談起。程序優(yōu)化最重要的就是找出待優(yōu)化的地方,也就是找出程序的哪些部分或者哪些模塊運(yùn)行緩慢亦或消耗大量的內(nèi)存。只有程序的各部分經(jīng)過了優(yōu)化,程序才能執(zhí)行的更快。

程序中運(yùn)行最多的部分,特別是那些被程序內(nèi)部循環(huán)重復(fù)調(diào)用的方法最該被優(yōu)化。

對(duì)于一個(gè)有經(jīng)驗(yàn)的碼農(nóng),發(fā)現(xiàn)程序中最需要被優(yōu)化的部分往往很簡(jiǎn)單。此外,還有很多工具可以幫助我們找出需要優(yōu)化的部分。我使用過Visual C++內(nèi)置的性能工具profiler來(lái)找出程序中消耗最多內(nèi)存的地方。

另一個(gè)我使用過的工具是英特爾的Vtune,它也能很好的檢測(cè)出程序中運(yùn)行最慢的部分。根據(jù)我的經(jīng)驗(yàn),內(nèi)部或嵌套循環(huán),調(diào)用第三方庫(kù)的方法通常是導(dǎo)致程序運(yùn)行緩慢的最主要的起因。

整形數(shù)

如果我們確定整數(shù)非負(fù),就應(yīng)該使用unsigned int而不是int。有些處理器處理無(wú)符號(hào)unsigned 整形數(shù)的效率遠(yuǎn)遠(yuǎn)高于有符號(hào)signed整形數(shù)(這是一種很好的做法,也有利于代碼具體類型的自解釋)。

因此,在一個(gè)緊密循環(huán)中,聲明一個(gè)int整形變量的最好方法是:

registerunsignedintvariable_name;

記住,整形in的運(yùn)算速度高浮點(diǎn)型float,并且可以被處理器直接完成運(yùn)算,而不需要借助于FPU(浮點(diǎn)運(yùn)算單元)或者浮點(diǎn)型運(yùn)算庫(kù)。

盡管這不保證編譯器一定會(huì)使用到寄存器存儲(chǔ)變量,也不能保證處理器處理能更高效處理unsigned整型,但這對(duì)于所有的編譯器是通用的。

例如在一個(gè)計(jì)算包中,如果需要結(jié)果精確到小數(shù)點(diǎn)后兩位,我們可以將其乘以100,然后盡可能晚的把它轉(zhuǎn)換為浮點(diǎn)型數(shù)字。

除法和取余數(shù)

在標(biāo)準(zhǔn)處理器中,對(duì)于分子和分母,一個(gè)32位的除法需要使用20至140次循環(huán)操作。除法函數(shù)消耗的時(shí)間包括一個(gè)常量時(shí)間加上每一位除法消耗的時(shí)間。

Time(numerator/denominator)=C0+C1*log2(numerator/denominator) =C0+C1*(log2(numerator)-log2(denominator)).

對(duì)于ARM處理器,這個(gè)版本需要20+4.3N次循環(huán)。這是一個(gè)消耗很大的操作,應(yīng)該盡可能的避免執(zhí)行。有時(shí),可以通過乘法表達(dá)式來(lái)替代除法。

例如,假如我們知道b是正數(shù)并且bc是個(gè)整數(shù),那么(a/b)>c可以改寫為a>(cb)。如果確定操作數(shù)是無(wú)符號(hào)unsigned的,使用無(wú)符號(hào)unsigned除法更好一些,因?yàn)樗扔蟹?hào)signed除法效率高。

合并除法和取余數(shù)

在一些場(chǎng)景中,同時(shí)需要除法(x/y)和取余數(shù)(x%y)操作。這種情況下,編譯器可以通過調(diào)用一次除法操作返回除法的結(jié)果和余數(shù)。如果既需要除法的結(jié)果又需要余數(shù),我們可以將它們寫在一起,如下所示:

intfunc_div_and_mod(inta,intb) { return(a/b)+(a%b); }

通過2的冪次進(jìn)行除法和取余數(shù)

如果除法中的除數(shù)是2的冪次,我們可以更好的優(yōu)化除法。編譯器使用移位操作來(lái)執(zhí)行除法。因此,我們需要盡可能的設(shè)置除數(shù)為2的冪次(例如64而不是66)。并且依然記住,無(wú)符號(hào)unsigned整數(shù)除法執(zhí)行效率高于有符號(hào)signed整形出發(fā)。

typedefunsignedintuint; uintdiv32u(uinta) { returna/32; } intdiv32s(inta) { returna/32; }

上面兩種除法都避免直接調(diào)用除法函數(shù),并且無(wú)符號(hào)unsigned的除法使用更少的計(jì)算機(jī)指令。由于需要移位到0和負(fù)數(shù),有符號(hào)signed的除法需要更多的時(shí)間執(zhí)行。

取模的一種替代方法

我們使用取余數(shù)操作符來(lái)提供算數(shù)取模。但有時(shí)可以結(jié)合使用if語(yǔ)句進(jìn)行取模操作??紤]如下兩個(gè)例子:

uintmodulo_func1(uintcount) { return(++count%60); } uintmodulo_func2(uintcount) { if(++count>=60) count=0; return(count); }

優(yōu)先使用if語(yǔ)句,而不是取余數(shù)運(yùn)算符,因?yàn)閕f語(yǔ)句的執(zhí)行速度更快。這里注意新版本函數(shù)只有在我們知道輸入的count結(jié)余0至59時(shí)在能正確的工作。

使用數(shù)組下標(biāo)

如果你想給一個(gè)變量設(shè)置一個(gè)代表某種意思的字符值,你可能會(huì)這樣做:

switch(queue) { case0:letter='W'; break; case1:letter='S'; break; case2:letter='U'; break; }

或者這樣做:

if(queue==0) letter='W'; elseif(queue==1) letter='S'; elseletter='U';

一種更簡(jiǎn)潔、更快的方法是使用數(shù)組下標(biāo)獲取字符數(shù)組的值。如下:

staticchar*classes="WSU"; letter=classes[queue];

全局變量

全局變量絕不會(huì)位于寄存器中。使用指針或者函數(shù)調(diào)用,可以直接修改全局變量的值。因此,編譯器不能將全局變量的值緩存在寄存器中,但這在使用全局變量時(shí)便需要額外的(常常是不必要的)讀取和存儲(chǔ)。所以,在重要的循環(huán)中我們不建議使用全局變量。

如果函數(shù)過多的使用全局變量,比較好的做法是拷貝全局變量的值到局部變量,這樣它才可以存放在寄存器。這種方法僅僅適用于全局變量不會(huì)被我們調(diào)用的任意函數(shù)使用。例子如下:

intf(void); intg(void); interrs; voidtest1(void) { errs+=f(); errs+=g(); } voidtest2(void) { intlocalerrs=errs; localerrs+=f(); localerrs+=g(); errs=localerrs; }

注意,test1必須在每次增加操作時(shí)加載并存儲(chǔ)全局變量errs的值,而test2存儲(chǔ)localerrs于寄存器并且只需要一個(gè)計(jì)算機(jī)指令。

使用別名

考慮如下的例子:

voidfunc1(int*data) { inti; for(i=0;i<10;?i++)???? ????{?????????? ????????anyfunc(?*data,?i);???? ????} }

盡管*data的值可能從未被改變,但編譯器并不知道anyfunc函數(shù)不會(huì)修改它,所以程序必須在每次使用它的時(shí)候從內(nèi)存中讀取它。如果我們知道變量的值不會(huì)被改變,那么就應(yīng)該使用如下的編碼:

voidfunc1(int*data) { inti; intlocaldata; localdata=*data; for(i=0;i<10;?i++)???? ????{?????????? ????????anyfunc?(localdata,?i);???? ????} }

這為編譯器優(yōu)化代碼提供了條件。

變量的生命周期分割

由于處理器中寄存器是固定長(zhǎng)度的,程序中數(shù)字型變量在寄存器中的存儲(chǔ)是有一定限制的。

有些編譯器支持“生命周期分割”(live-range splitting),也就是說在程序的不同部分,變量可以被分配到不同的寄存器或者內(nèi)存中。變量的生命周期開始于對(duì)它進(jìn)行的最后一次賦值,結(jié)束于下次賦值前的最后一次使用。

在生命周期內(nèi),變量的值是有效的,也就是說變量是活著的。不同生命周期之間,變量的值是不被需要的,也就是說變量是死掉的。這樣,寄存器就可以被其余變量使用,從而允許編譯器分配更多的變量使用寄存器。

需要使用寄存器分配的變量數(shù)目需要超過函數(shù)中不同變量生命周期的個(gè)數(shù)。如果不同變量生命周期的個(gè)數(shù)超過了寄存器的數(shù)目,那么一些變量必須臨時(shí)存儲(chǔ)于內(nèi)存。這個(gè)過程就稱之為分割。

編譯器首先分割最近使用的變量,用以降低分割帶來(lái)的消耗。禁止變量生命周期分割的方法如下:

限定變量的使用數(shù)量:這個(gè)可以通過保持函數(shù)中的表達(dá)式簡(jiǎn)單、小巧、不使用太多的變量實(shí)現(xiàn)。將較大的函數(shù)拆分為小而簡(jiǎn)單的函數(shù)也會(huì)達(dá)到很好的效果。

對(duì)經(jīng)常使用到的變量采用寄存器存儲(chǔ):這樣允許我們告訴編譯器該變量是需要經(jīng)常使用的,所以需要優(yōu)先存儲(chǔ)于寄存器中。然而,在某種情況下,這樣的變量依然可能會(huì)被分割出寄存器。

變量類型

C編譯器支持基本類型:char、short、int、long(包括有符號(hào)signed和無(wú)符號(hào)unsigned)、float和double。使用正確的變量類型至關(guān)重要,因?yàn)檫@可以減少代碼和數(shù)據(jù)的大小并大幅增加程序的性能。

局部變量

我們應(yīng)該盡可能的不使用char和short類型的局部變量。對(duì)于char和short類型,編譯器需要在每次賦值的時(shí)候?qū)⒕植孔兞繙p少到8或者16位。

這對(duì)于有符號(hào)變量稱之為有符號(hào)擴(kuò)展,對(duì)于無(wú)符號(hào)變量稱之為零擴(kuò)展。這些擴(kuò)展可以通過寄存器左移24或者16位,然后根據(jù)有無(wú)符號(hào)標(biāo)志右移相同的位數(shù)實(shí)現(xiàn),這會(huì)消耗兩次計(jì)算機(jī)指令操作(無(wú)符號(hào)char類型的零擴(kuò)展僅需要消耗一次計(jì)算機(jī)指令)。

可以通過使用int和unsigned int類型的局部變量來(lái)避免這樣的移位操作。這對(duì)于先加載數(shù)據(jù)到局部變量,然后處理局部變量數(shù)據(jù)值這樣的操作非常重要。無(wú)論輸入輸出數(shù)據(jù)是8位或者16位,將它們考慮為32位是值得的。

考慮下面的三個(gè)函數(shù):

intwordinc(inta) { returna+1; } shortshortinc(shorta) { returna+1; } charcharinc(chara) { returna+1; }

盡管結(jié)果均相同,但是第一個(gè)程序片段運(yùn)行速度高于后兩者。

指針

我們應(yīng)該盡可能的使用引用值的方式傳遞結(jié)構(gòu)數(shù)據(jù),也就是說使用指針,否則傳遞的數(shù)據(jù)會(huì)被拷貝到棧中,從而降低程序的性能。我曾見過一個(gè)程序采用傳值的方式傳遞非常大的結(jié)構(gòu)數(shù)據(jù),然后這可以通過一個(gè)簡(jiǎn)單的指針更好的完成。

函數(shù)通過參數(shù)接受結(jié)構(gòu)數(shù)據(jù)的指針,如果我們確定不改變數(shù)據(jù)的值,我們需要將指針指向的內(nèi)容定義為常量。例如:

voidprint_data_of_a_structure(constThestruct*data_pointer) { ...printfcontentsofthestructure... }

這個(gè)示例告訴編譯器函數(shù)不會(huì)改變外部參數(shù)的值(使用const修飾),并且不用在每次訪問時(shí)都進(jìn)行讀取。同時(shí),確保編譯器限制任何對(duì)只讀結(jié)構(gòu)的修改操作從而給予結(jié)構(gòu)數(shù)據(jù)額外的保護(hù)。

指針鏈

指針鏈經(jīng)常被用于訪問結(jié)構(gòu)數(shù)據(jù)。例如,常用的代碼如下:

typedefstruct{intx,y,z;}Point3; typedefstruct{Point3*pos,*direction;}Object; voidInitPos1(Object*p) { p->pos->x=0; p->pos->y=0; p->pos->z=0; }

然而,這種的代碼在每次操作時(shí)必須重復(fù)調(diào)用p->pos,因?yàn)榫幾g器不知道p->pos->x與p->pos是相同的。一種更好的方法是緩存p->pos到一個(gè)局部變量:

voidInitPos2(Object*p) { Point3*pos=p->pos; pos->x=0; pos->y=0; pos->z=0; }

另一種方法是在Object結(jié)構(gòu)中直接包含Point3類型的數(shù)據(jù),這能完全消除對(duì)Point3使用指針操作。

條件執(zhí)行

條件執(zhí)行語(yǔ)句大多在if語(yǔ)句中使用,也在使用關(guān)系運(yùn)算符(<,==,>等)或者布爾值表達(dá)式(&&,!等)計(jì)算復(fù)雜表達(dá)式時(shí)使用。對(duì)于包含函數(shù)調(diào)用的代碼片段,由于函數(shù)返回值會(huì)被銷毀,因此條件執(zhí)行是無(wú)效的。

因此,保持if和else語(yǔ)句盡可能簡(jiǎn)單是十分有益處的,因?yàn)檫@樣編譯器可以集中處理它們。關(guān)系表達(dá)式應(yīng)該寫在一起。

下面的例子展示編譯器如何使用條件執(zhí)行:

intg(inta,intb,intc,intd) { if(a>0&&b>0&&c

由于條件被聚集到一起,編譯器能夠?qū)⑺麄兗刑幚怼?/p>

布爾表達(dá)式和范圍檢查

一個(gè)常用的布爾表達(dá)式是用于判斷變量是否位于某個(gè)范圍內(nèi),例如,檢查一個(gè)圖形坐標(biāo)是否位于一個(gè)窗口內(nèi):

boolPointInRectangelArea(Pointp,Rectangle*r) { return(p.x>=r->xmin&&p.xxmax&& p.y>=r->ymin&&p.yymax); }

這里有一種更快的方法:x>min && x

boolPointInRectangelArea(Pointp,Rectangle*r) { return((unsigned)(p.x-r->xmin)xmax&& (unsigned)(p.y-r->ymin)ymax); }

布爾表達(dá)式和零值比較

處理器的標(biāo)志位在比較指令操作后被設(shè)置。標(biāo)志位同樣可以被諸如MOV、ADD、AND、MUL等基本算術(shù)和裸機(jī)指令改寫。如果數(shù)據(jù)指令設(shè)置了標(biāo)志位,N和Z標(biāo)志位也將與結(jié)果與0比較一樣進(jìn)行設(shè)置。N標(biāo)志表示結(jié)果是否是負(fù)值,Z標(biāo)志表示結(jié)果是否是0。

C語(yǔ)言中,處理器中的N和Z標(biāo)志位與下面的指令聯(lián)系在一起:有符號(hào)關(guān)系運(yùn)算x<0,x>=0,x==0,x!=0;無(wú)符號(hào)關(guān)系運(yùn)算x==0,x!=0(或者x>0)。

C代碼中每次關(guān)系運(yùn)算符的調(diào)用,編譯器都會(huì)發(fā)出一個(gè)比較指令。如果操作符是上面提到的,編譯器便會(huì)優(yōu)化掉比較指令。例如:

intaFunction(intx,inty) { if(x+y

盡可能的使用上面的判斷方式,這可以在關(guān)鍵循環(huán)中減少比較指令的調(diào)用,進(jìn)而減少代碼體積并提高代碼性能。C語(yǔ)言沒有借位和溢出位的概念,因此,如果不借助匯編,不可能直接使用借位標(biāo)志C和溢出位標(biāo)志V。但編譯器支持借位(無(wú)符號(hào)溢出),例如:

intsum(intx,inty) { intres; res=x+y; if((unsigned)res

懶檢測(cè)開發(fā)

在if(a>10 && b=4)這樣的語(yǔ)句中,確保AND表達(dá)式的第一部分最可能較快的給出結(jié)果(或者最早、最快計(jì)算),這樣第二部分便有可能不需要執(zhí)行。

用switch()函數(shù)替代if…else…

對(duì)于涉及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()語(yǔ)句中,如果最后一條語(yǔ)句命中,之前的條件都需要被測(cè)試執(zhí)行一次。Switch允許我們不做額外的測(cè)試。如果必須使用if…else…語(yǔ)句,將最可能執(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(a<=4)?{ ????if(a==1)?????{ ????}??else?if(a==2)??{ ????}??else?if(a==3)??{ ????}??else?if(a==4)???{ ????} } else { ????if(a==5)??{ ????}?else?if(a==6)???{ ????}?else?if(a==7)??{ ????}?else?if(a==8)??{ ????} }

或者如下:

if(a<=4) { ????if(a<=2) ????{ ????????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(a<=6) ????{ ????????if(a==5) ????????{ ????????????/*?a?is?5?*/ ????????} ????????else ????????{ ????????????/*?a?must?be?6?*/ ????????} ????} ????else ????{ ????????if(a==7) ????????{ ????????????/*?a?is?7?*/ ????????} ????????else ????????{ ????????????/*?a?must?be?8?*/ ????????} ????} }

比較如下兩種case語(yǔ)句:

switch語(yǔ)句vs查找表

Switch的應(yīng)用場(chǎng)景如下:

調(diào)用一到多個(gè)函數(shù)

設(shè)置變量值或者返回一個(gè)值

執(zhí)行一到多個(gè)代碼片段

如果case標(biāo)簽很多,在switch的前兩個(gè)使用場(chǎng)景中,使用查找表可以更高效的完成。例如下面的兩種轉(zhuǎn)換字符串的方式:

char*Condition_String1(intcondition){ switch(condition){ case0:return"EQ"; case1:return"NE"; case2:return"CS"; case3:return"CC"; case4:return"MI"; case5:return"PL"; case6:return"VS"; case7:return"VC"; case8:return"HI"; case9:return"LS"; case10:return"GE"; case11:return"LT"; case12:return"GT"; case13:return"LE"; case14:return""; default:return0; } } char*Condition_String2(intcondition){ if((unsigned)condition>=15)return0; return "EQNECSCCMIPLVSVCHILSGELTGTLE"+ 3*condition; }

第一個(gè)程序需要240 bytes,而第二個(gè)僅僅需要72 bytes。

循環(huán)

循環(huán)是大多數(shù)程序中的常用的結(jié)構(gòu);程序執(zhí)行的大部分時(shí)間發(fā)生在循環(huán)中,因此十分值得在循環(huán)執(zhí)行時(shí)間上下一番功夫。

循環(huán)終止

如果不加注意,循環(huán)終止條件的編寫會(huì)導(dǎo)致額外的負(fù)擔(dān)。我們應(yīng)該使用計(jì)數(shù)到零的循環(huán)和簡(jiǎn)單的循環(huán)終止條件。簡(jiǎn)單的終止條件消耗更少的時(shí)間。看下面計(jì)算n!的兩個(gè)程序。第一個(gè)實(shí)現(xiàn)使用遞增的循環(huán),第二個(gè)實(shí)現(xiàn)使用遞減循環(huán)。

intfact1_func(intn) { inti,fact=1; for(i=1;i<=?n;?i++) ??????fact?*=?i; ????return?(fact); } ? int?fact2_func(int?n) { ????int?i,?fact?=?1; ????for?(i?=?n;?i?!=?0;?i--) ???????fact?*=?i; ????return?(fact); }

第二個(gè)程序的fact2_func執(zhí)行效率高于第一個(gè)。

更快的for()循環(huán)

這是一個(gè)簡(jiǎn)單而高效的概念。通常,我們編寫for循環(huán)代碼如下:

for(i=0;i<10;??i++){?...?}

i從0循環(huán)到9。如果我們不介意循環(huán)計(jì)數(shù)的順序,我們可以這樣寫:

for(i=10;i--;){...}

這樣快的原因是因?yàn)樗芨斓奶幚韎的值–測(cè)試條件是:i是非零的嗎?如果這樣,遞減i的值。對(duì)于上面的代碼,處理器需要計(jì)算“計(jì)算i減去10,其值非負(fù)嗎?如果非負(fù),i遞增并繼續(xù)”。簡(jiǎn)單的循環(huán)卻有很大的不同。這樣,i從9遞減到0,這樣的循環(huán)執(zhí)行速度更快。

這里的語(yǔ)法有點(diǎn)奇怪,但確實(shí)合法的。循環(huán)中的第三條語(yǔ)句是可選的(無(wú)限循環(huán)可以寫為for(;;))。如下代碼擁有同樣的效果:

for(i=10;i;i--){}

或者更進(jìn)一步的:

for(i=10;i!=0;i--){}

這里我們需要記住的是循環(huán)必須終止于0(因此,如果在50到80之間循環(huán),這不會(huì)起作用),并且循環(huán)計(jì)數(shù)器是遞減的。使用遞增循環(huán)計(jì)數(shù)器的代碼不享有這種優(yōu)化。

合并循環(huán)

如果一個(gè)循環(huán)能解決問題堅(jiān)決不用二個(gè)。但如果你需要在循環(huán)中做很多工作,這坑你并不適合處理器的指令緩存。這種情況下,兩個(gè)分開的循環(huán)可能會(huì)比單個(gè)循環(huán)執(zhí)行的更快。下面是一個(gè)例子:

函數(shù)循環(huán)

調(diào)用函數(shù)時(shí)總是會(huì)有一定的性能消耗。不僅程序指針需要改變,而且使用的變量需要壓棧并分配新變量。為提升程序的性能,在函數(shù)這點(diǎn)上有很多可以優(yōu)化的。在保持程序代碼可讀性的同時(shí)也需要代碼的大小是可控的。

如果在循環(huán)中一個(gè)函數(shù)經(jīng)常被調(diào)用,那么就將循環(huán)納入到函數(shù)中,這樣可以減少重復(fù)的函數(shù)調(diào)用。代碼如下:

for(i=0;i<100?;?i++) { ????func(t,i); } - - - void?func(int?w,d) { ????lots?of?stuff. }

應(yīng)改為:

func(t); - - - voidfunc(w) { for(i=0;i<100?;?i++) ????{ ????????//lots?of?stuff. ????} }

循環(huán)展開

簡(jiǎn)單的循環(huán)可以展開以獲取更好的性能,但需要付出代碼體積增加的代價(jià)。循環(huán)展開后,循環(huán)計(jì)數(shù)應(yīng)該越來(lái)越小從而執(zhí)行更少的代碼分支。如果循環(huán)迭代次數(shù)只有幾次,那么可以完全展開循環(huán),以便消除循壞帶來(lái)的負(fù)擔(dān)。

這會(huì)帶來(lái)很大的不同。循環(huán)展開可以帶非??捎^的節(jié)省性能,原因是代碼不用每次循環(huán)需要檢查和增加i的值。例如:

編譯器通常會(huì)像上面那樣展開簡(jiǎn)單的,迭代次數(shù)固定的循環(huán)。但是像下面的代碼:

for(i=0;i

下面的代碼(Example 1)明顯比使用循環(huán)的方式寫的更長(zhǎng),但卻更有效率。block-sie的值設(shè)置為8僅僅適用于測(cè)試的目的,只要我們重復(fù)執(zhí)行“l(fā)oop-contents”相同的次數(shù),都會(huì)有很好的效果。

在這個(gè)例子中,循環(huán)條件每8次迭代才會(huì)被檢查,而不是每次都進(jìn)行檢查。由于不知道迭代的次數(shù),一般不會(huì)被展開。因此,盡可能的展開循環(huán)可以讓我們獲得更好的執(zhí)行速度。

//Example1 #include #defineBLOCKSIZE(8) voidmain(void) { inti=0; intlimit=33;/*couldbeanything*/ intblocklimit; /*ThelimitmaynotbedivisiblebyBLOCKSIZE, *goasnearaswecanfirst,thentidyup. */ blocklimit=(limit/BLOCKSIZE)*BLOCKSIZE; /*unrolltheloopinblocksof8*/ while(i

統(tǒng)計(jì)非零位的數(shù)量

通過不斷的左移,提取并統(tǒng)計(jì)最低位,示例程序1高效的檢查一個(gè)數(shù)組中有幾個(gè)非零位。示例程序2被循環(huán)展開四次,然后通過將四次移位合并成一次來(lái)優(yōu)化代碼。經(jīng)常展開循環(huán),可以提供很多優(yōu)化的機(jī)會(huì)。

//Example-1 intcountbit1(uintn) { intbits=0; while(n!=0) { if(n&1)bits++; n>>=1; } returnbits; } //Example-2 intcountbit2(uintn) { intbits=0; while(n!=0) { if(n&1)bits++; if(n&2)bits++; if(n&4)bits++; if(n&8)bits++; n>>=4; } returnbits; }

盡早的斷開循環(huán)

通常,循環(huán)并不需要全部都執(zhí)行。例如,如果我們?cè)趶臄?shù)組中查找一個(gè)特殊的值,一經(jīng)找到,我們應(yīng)該盡可能早的斷開循環(huán)。例如:如下循環(huán)從10000個(gè)整數(shù)中查找是否存在-99。

found=FALSE; for(i=0;i<10000;i++) { ????if(?list[i]?==?-99?) ????{ ????????found?=?TRUE; ????} } ? if(?found?)? ????printf("Yes,?there?is?a?-99.?Hooray! ");

上面的代碼可以正常工作,但是需要循環(huán)全部執(zhí)行完畢,而不論是否我們已經(jīng)查找到。更好的方法是一旦找到我們查找的數(shù)字就終止繼續(xù)查詢。

found=FALSE; for(i=0;i<10000;?i++) { ????if(?list[i]?==?-99?) ????{ ????????found?=?TRUE; ????????break; ????} } if(?found?)? ????printf("Yes,?there?is?a?-99.?Hooray! ");

假如待查數(shù)據(jù)位于第23個(gè)位置上,程序便會(huì)執(zhí)行23次,從而節(jié)省9977次循環(huán)。

函數(shù)設(shè)計(jì)

設(shè)計(jì)小而簡(jiǎn)單的函數(shù)是個(gè)很好的習(xí)慣。這允許寄存器可以執(zhí)行一些諸如寄存器變量申請(qǐng)的優(yōu)化,是非常高效的。

函數(shù)調(diào)用的性能消耗

函數(shù)調(diào)用對(duì)于處理器的性能消耗是很小的,只占有函數(shù)執(zhí)行工作中性能消耗的一小部分。參數(shù)傳入函數(shù)變量寄存器中有一定的限制。這些參數(shù)必須是整型兼容的(char,shorts,ints和floats都占用一個(gè)字)或者小于四個(gè)字大?。òㄕ加?個(gè)字的doubles和long longs)。

如果參數(shù)限制個(gè)數(shù)為4,那么第五個(gè)和之后的字就會(huì)存儲(chǔ)在棧上。這便在調(diào)用函數(shù)是需要從棧上加載參數(shù)從而增加存儲(chǔ)和讀取的消耗。

看下面的代碼:

intf1(inta,intb,intc,intd){ returna+b+c+d; } intg1(void){ returnf1(1,2,3,4); } intf2(inta,intb,intc,intd,inte,intf){ returna+b+c+d+e+f; } ingg2(void){ returnf2(1,2,3,4,5,6); }

函數(shù)g2中的第五個(gè)和第六個(gè)參數(shù)存儲(chǔ)于棧上并在函數(shù)f2中進(jìn)行加載,會(huì)多消耗2個(gè)參數(shù)的存儲(chǔ)。

減少函數(shù)參數(shù)傳遞消耗

減少函數(shù)參數(shù)傳遞消耗的方法有:

盡量保證函數(shù)使用少于四個(gè)參數(shù)。這樣就不會(huì)使用棧來(lái)存儲(chǔ)參數(shù)值。

如果函數(shù)需要多于四個(gè)的參數(shù),盡量確保使用后面參數(shù)的價(jià)值高于讓其存儲(chǔ)于棧所付出的代價(jià)。

通過指針傳遞參數(shù)的引用而不是傳遞參數(shù)結(jié)構(gòu)體本身。

將參數(shù)放入一個(gè)結(jié)構(gòu)體并通過指針傳入函數(shù),這樣可以減少參數(shù)的數(shù)量并提高可讀性。

盡量少用占用兩個(gè)字大小的long類型參數(shù)。對(duì)于需要浮點(diǎn)類型的程序,double也因?yàn)檎加脙蓚€(gè)字大小而應(yīng)盡量少用。

避免函數(shù)參數(shù)既存在于寄存器又存在于棧中(稱之為參數(shù)拆分)?,F(xiàn)在的編譯器對(duì)這種情況處理的不夠高效:所有的寄存器變量也會(huì)放入到棧中。

避免變參。變參函數(shù)將參數(shù)全部放入棧。

葉子函數(shù)

不調(diào)用任何函數(shù)的函數(shù)稱之為葉子函數(shù)。在以下應(yīng)用中,近一半的函數(shù)調(diào)用是調(diào)用葉子函數(shù)。由于不需要執(zhí)行寄存器變量的存儲(chǔ)和讀取,葉子函數(shù)在任何平臺(tái)都很高效。

寄存器變量讀取的性能消耗,相比于使用四五個(gè)寄存器變量的葉子函數(shù)所做的工作帶來(lái)的系能消耗是非常小的。所以盡可能的將經(jīng)常調(diào)用的函數(shù)寫成葉子函數(shù)。函數(shù)調(diào)用的次數(shù)可以通過一些工具檢查。

下面是一些將一個(gè)函數(shù)編譯為葉子函數(shù)的方法:

避免調(diào)用其他函數(shù):包括那些轉(zhuǎn)而調(diào)用C庫(kù)的函數(shù)(比如除法或者浮點(diǎn)數(shù)操作函數(shù))。

對(duì)于簡(jiǎn)短的函數(shù)使用__inline修飾()。

內(nèi)聯(lián)函數(shù)

內(nèi)聯(lián)函數(shù)禁用所有的編譯選項(xiàng)。使用__inline修飾函數(shù)導(dǎo)致函數(shù)在調(diào)用處直接替換為函數(shù)體。這樣代碼調(diào)用函數(shù)更快,但增加代碼的大小,特別在函數(shù)本身比較大而且經(jīng)常調(diào)用的情況下。

__inlineintsquare(intx){ returnx*x; } #include doublelength(intx,inty){ returnsqrt(square(x)+square(y)); }

使用內(nèi)聯(lián)函數(shù)的好處如下:

沒有函數(shù)調(diào)用負(fù)擔(dān)。函數(shù)調(diào)用處直接替換為函數(shù)體,因此沒有諸如讀取寄存器變量等性能消耗。

更小的參數(shù)傳遞消耗。由于不需要拷貝變量,傳遞參數(shù)的消耗更小。如果參數(shù)是常量,編譯器可以提供更好的優(yōu)化。

內(nèi)聯(lián)函數(shù)的缺陷是如果調(diào)用的地方很多,代碼的體積會(huì)變得很大。這主要取決于函數(shù)本身的大小和調(diào)用的次數(shù)。

僅對(duì)重要的函數(shù)使用inline是明智的。如果使用得當(dāng),內(nèi)聯(lián)函數(shù)甚至可以減少代碼的體積:函數(shù)調(diào)用會(huì)產(chǎn)生一些計(jì)算機(jī)指令,但是使用內(nèi)聯(lián)的優(yōu)化版本可能產(chǎn)生更少的計(jì)算機(jī)指令。

使用查找表

函數(shù)通??梢栽O(shè)計(jì)成查找表,這樣可以顯著提升性能。查找表的精確度比通常的計(jì)算低,但對(duì)于一般的程序并沒什么差異。

許多信號(hào)處理程序(例如,調(diào)制解調(diào)器解調(diào)軟件)使用很多非常消耗計(jì)算性能的sin和cos函數(shù)。對(duì)于實(shí)時(shí)系統(tǒng),精確性不是特別重要,sin、cos查找表可能更合適。當(dāng)使用查找表時(shí),盡可能將相似的操作放入查找表,這樣比使用多個(gè)查找表更快,更能節(jié)省存儲(chǔ)空間。

浮點(diǎn)運(yùn)算

盡管浮點(diǎn)運(yùn)算對(duì)于所有的處理器都很耗時(shí),但對(duì)于實(shí)現(xiàn)信號(hào)處理軟件時(shí)我們?nèi)匀恍枰褂?。在編寫浮點(diǎn)操作程序時(shí),記住如下幾點(diǎn):

浮點(diǎn)除法很慢。浮點(diǎn)除法比加法或者乘法慢兩倍。通過使用常量將除法轉(zhuǎn)換為乘法(例如,x=x/3.0可以替換為x=x*(1.0/3.0))。常量的除法在編譯期間計(jì)算。

使用float代替double。Float類型的變量消耗更好的內(nèi)存和寄存器,并由于精度低而更加高效。如果精度夠用,盡可能使用float。

避免使用先驗(yàn)函數(shù)。先驗(yàn)函數(shù),例如sin、exp和log是通過一系列的乘法和加法實(shí)現(xiàn)的(使用了精度擴(kuò)展)。這些操作比通常的乘法至少慢十倍。

簡(jiǎn)化浮點(diǎn)運(yùn)算表達(dá)式。編譯器并不能將應(yīng)用于整型操作的優(yōu)化手段應(yīng)用于浮點(diǎn)操作。例如,3*(x/3)可以優(yōu)化為x,而浮點(diǎn)運(yùn)算就會(huì)損失精度。因此,如果知道結(jié)果正確,進(jìn)行必要手工浮點(diǎn)優(yōu)化是有必要的。

然而,浮點(diǎn)運(yùn)算的表現(xiàn)可能不能滿足特定軟件對(duì)性能的需求。這種情況下,最好的辦法或許是使用定點(diǎn)算數(shù)運(yùn)算。當(dāng)值的范圍足夠小,定點(diǎn)算數(shù)操作比浮點(diǎn)運(yùn)算更精確、更快速。

其他技巧

通常,可以使用空間換時(shí)間。如果你能緩存經(jīng)常用的數(shù)據(jù)而不是重新計(jì)算,這便能更快的訪問。比如sine和cosine查找表,或者偽隨機(jī)數(shù)。

盡量不在循環(huán)中使用++和–。例如:while(n–){},這有時(shí)難于優(yōu)化。

減少全局變量的使用。

除非像聲明為全局變量,使用static修飾變量為文件內(nèi)訪問。

盡可能使用一個(gè)字大小的變量(int、long等),使用它們(而不是char,short,double,位域等)機(jī)器可能運(yùn)行的更快。

不使用遞歸。遞歸可能優(yōu)雅而簡(jiǎn)單,但需要太多的函數(shù)調(diào)用。

不在循環(huán)中使用sqrt開平方函數(shù),計(jì)算平方根非常消耗性能。

一維數(shù)組比多維數(shù)組更快。

編譯器可以在一個(gè)文件中進(jìn)行優(yōu)化-避免將相關(guān)的函數(shù)拆分到不同的文件中,如果將它們放在一起,編譯器可以更好的處理它們(例如可以使用inline)。

單精度函數(shù)比雙精度更快。

浮點(diǎn)乘法運(yùn)算比浮點(diǎn)除法運(yùn)算更快-使用val*0.5而不是val/2.0。

加法操作比乘法快-使用val+val+val而不是val*3。

put()函數(shù)比printf()快,但不靈活。

使用#define宏取代常用的小函數(shù)。

二進(jìn)制/未格式化的文件訪問比格式化的文件訪問更快,因?yàn)槌绦虿恍枰谌藶榭勺x的ASCII和機(jī)器可讀的二進(jìn)制之間轉(zhuǎn)化。如果你不需要閱讀文件的內(nèi)容,將它保存為二進(jìn)制。

如果你的庫(kù)支持mallopt()函數(shù)(用于控制malloc),盡量使用它。MAXFAST的設(shè)置,對(duì)于調(diào)用很多次malloc工作的函數(shù)由很大的性能提升。如果一個(gè)結(jié)構(gòu)一秒鐘內(nèi)需要多次創(chuàng)建并銷毀,試著設(shè)置mallopt選項(xiàng)。

最后,但是是最重要的是-將編譯器優(yōu)化選項(xiàng)打開!看上去很顯而易見,但卻經(jīng)常在產(chǎn)品推出時(shí)被忘記。編譯器能夠在更底層上對(duì)代碼進(jìn)行優(yōu)化,并針對(duì)目標(biāo)處理器執(zhí)行特定的優(yōu)化處理。

責(zé)任編輯:lq

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • C語(yǔ)言
    +關(guān)注

    關(guān)注

    180

    文章

    7595

    瀏覽量

    135925
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4286

    瀏覽量

    62337
  • 編譯器
    +關(guān)注

    關(guān)注

    1

    文章

    1617

    瀏覽量

    49019

原文標(biāo)題:精品干貨:C語(yǔ)言的高效編程與代碼優(yōu)化

文章出處:【微信號(hào):gh_c472c2199c88,微信公眾號(hào):嵌入式微處理器】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    AIC3254的miniDSP編寫代碼和編寫C5502代碼有什么區(qū)別?

    問題:AIC3254的miniDSP編寫代碼和編寫C5502代碼有什么區(qū)別,執(zhí)行速度代碼來(lái)
    發(fā)表于 11-06 07:22

    C語(yǔ)言指針學(xué)習(xí)筆記

    本文底層內(nèi)存分析,徹底讓讀者明白C語(yǔ)言指針的本質(zhì)。
    的頭像 發(fā)表于 11-05 17:40 ?126次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語(yǔ)言</b>指針學(xué)習(xí)筆記

    C語(yǔ)言與Java語(yǔ)言的對(duì)比

    C語(yǔ)言和Java語(yǔ)言都是當(dāng)前編程領(lǐng)域中的重要成員,它們各自具有獨(dú)特的優(yōu)勢(shì)和特點(diǎn),適用于不同的應(yīng)用場(chǎng)景。以下將從語(yǔ)法特性、內(nèi)存管理、跨平臺(tái)性、性能、應(yīng)用領(lǐng)域等多個(gè)
    的頭像 發(fā)表于 10-29 17:31 ?159次閱讀

    Keil編譯器優(yōu)化方法

    我們都知道,代碼是可以通過編譯器優(yōu)化的,有的時(shí)候,為了提高運(yùn)行速度或者減少代碼尺寸,會(huì)開啟優(yōu)化選項(xiàng)。
    的頭像 發(fā)表于 10-23 16:35 ?270次閱讀
    Keil編譯器<b class='flag-5'>優(yōu)化</b>方法

    hex文件如何查看原c語(yǔ)言代碼

    是處理器可以直接執(zhí)行的指令,而 C 語(yǔ)言代碼則是人類可讀的高級(jí)編程語(yǔ)言代碼。 然而,如果你想要從
    的頭像 發(fā)表于 09-02 10:37 ?1242次閱讀

    SEGGER編譯器優(yōu)化和安全技術(shù)介紹 支持最新CC++語(yǔ)言

    代碼生成,SEGGER編譯器生成非常小的代碼,非常適合內(nèi)存受限的環(huán)境,而不會(huì)犧牲執(zhí)行速度。 2)?速度優(yōu)
    的頭像 發(fā)表于 06-04 15:31 ?1373次閱讀
    SEGGER編譯器<b class='flag-5'>優(yōu)化</b>和安全技術(shù)介紹 支持最新<b class='flag-5'>C</b>和<b class='flag-5'>C</b>++<b class='flag-5'>語(yǔ)言</b>

    G431CB把stack heap全分配到ccmram,代碼執(zhí)行速度并未改善是怎么回事?

    把stack heap全分配到ccmram發(fā)現(xiàn)函數(shù)執(zhí)行時(shí)間也沒有什么改善,附圖是我的結(jié)果 函數(shù)執(zhí)行速度非但沒有改善,反而發(fā)現(xiàn)在ccmram執(zhí)行很不穩(wěn)定; 測(cè)試手段:用定時(shí)器3計(jì)數(shù)來(lái)實(shí)現(xiàn)該函數(shù)
    發(fā)表于 03-27 08:23

    C語(yǔ)言內(nèi)存泄漏問題原理

    內(nèi)存泄漏問題只有在使用堆內(nèi)存的時(shí)候才會(huì)出現(xiàn),棧內(nèi)存不存在內(nèi)存泄漏問題,因?yàn)闂?b class='flag-5'>內(nèi)存會(huì)自動(dòng)分配和釋放。C
    發(fā)表于 03-19 11:38 ?455次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語(yǔ)言</b><b class='flag-5'>內(nèi)存</b>泄漏問題原理

    C語(yǔ)言中的動(dòng)態(tài)內(nèi)存管理講解

    本章將講解 C 中的動(dòng)態(tài)內(nèi)存管理。C 語(yǔ)言內(nèi)存的分配和管理提供了幾個(gè)函數(shù)。這些函數(shù)可以在 頭文件中找到。
    的頭像 發(fā)表于 02-23 14:03 ?361次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語(yǔ)言</b>中的動(dòng)態(tài)<b class='flag-5'>內(nèi)存</b>管理講解

    C語(yǔ)言運(yùn)行環(huán)境是什么

    C語(yǔ)言運(yùn)行環(huán)境(C language runtime environment)是指在執(zhí)行C語(yǔ)言
    的頭像 發(fā)表于 11-27 16:13 ?3292次閱讀

    C語(yǔ)言在單片機(jī)中是如何執(zhí)行

    或許我們平時(shí)大多數(shù)學(xué)習(xí)C語(yǔ)言都是在Windows環(huán)境下學(xué)習(xí)的,對(duì)于程序執(zhí)行的底層邏輯了解的不是非常清楚,所以本文在這里給大家介紹一下,C語(yǔ)言
    的頭像 發(fā)表于 11-24 15:45 ?2419次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語(yǔ)言</b>在單片機(jī)中是如何<b class='flag-5'>執(zhí)行</b>的

    c語(yǔ)言源程序main函數(shù)的位置

    C語(yǔ)言源程序中的main函數(shù)是程序的入口點(diǎn),它被認(rèn)為是C語(yǔ)言程序的起點(diǎn)。在執(zhí)行程序時(shí),操作系統(tǒng)將首先定位到main函數(shù),并從該函數(shù)開始
    的頭像 發(fā)表于 11-24 10:23 ?2267次閱讀

    c語(yǔ)言怎么把代碼全部注釋掉

    要將C語(yǔ)言代碼全部注釋掉,即不讓代碼被編譯和執(zhí)行,可以使用注釋語(yǔ)句來(lái)實(shí)現(xiàn)。
    的頭像 發(fā)表于 11-22 10:21 ?6480次閱讀

    c語(yǔ)言中順序結(jié)構(gòu)的基本流程

    C語(yǔ)言是一種通用的編程語(yǔ)言,注重結(jié)構(gòu)化編程和順序結(jié)構(gòu)。順序結(jié)構(gòu)是C語(yǔ)言編程中最簡(jiǎn)單也是最基本的控制結(jié)構(gòu)之一,它按照
    的頭像 發(fā)表于 11-22 10:20 ?915次閱讀

    C語(yǔ)言中如何優(yōu)化代碼執(zhí)行效率

    do...while循環(huán)大多數(shù)時(shí)候更可靠,具備更好的容錯(cuò)性。 適當(dāng)?shù)膶?duì)循環(huán)做展開 這是經(jīng)典的速度優(yōu)化,但許多編譯程序(如gcc -funroll-loops)能自動(dòng)完成這個(gè)事,所以現(xiàn)在你自己來(lái)
    的頭像 發(fā)表于 11-20 18:09 ?554次閱讀