高效的C編程之: 函數(shù)調(diào)用
函數(shù)設計的基本原則是使其函數(shù)體盡量的小。這樣編譯器可以對函數(shù)做更多的優(yōu)化。
14.9.1減少函數(shù)調(diào)用開銷ARM上的函數(shù)調(diào)用開銷比非RISC體系結構上的調(diào)用開銷?。?/p>
·調(diào)用返回指令“BL”或“MOVpc,lr”一般只需要6個指令周期(ARM7上)。
·在函數(shù)的入口和出口使用多寄存器加載/存儲指令LDM和STM(Thumb指令使用PUSH和POP)提高函數(shù)體的執(zhí)行效率。
ARM體系結構過程調(diào)用標準AAPCS定義了如何通過寄存器傳遞參數(shù)和返回值。函數(shù)中的前4個整型參數(shù)是通過ARM的前4個寄存器r0、r1、r2和r3來傳遞的。傳遞參數(shù)可以是與整型兼容的數(shù)據(jù)類型,如字符類型char、半字類型short等。
注意
如果是雙字類型,如longlong型,只能通過寄存器傳遞兩個參數(shù)。
不能通過寄存器傳遞的參數(shù),通過函數(shù)堆棧來傳遞。這樣不論是函數(shù)的調(diào)用者還是被調(diào)用者都必須通過訪問堆棧來訪問參數(shù),使程序的執(zhí)行效率下降。
下面的例子顯示了函數(shù)調(diào)用是傳遞4個參數(shù)和多于4個參數(shù)的區(qū)別。
傳遞4個參數(shù)的函數(shù)調(diào)用源文件如下。
intfunc1(inta,intb,intc,intd)
{
returna+b+c+d;
}
intcaller1(void)
{
returnfunc1(1,2,3,4);
}
編譯的結果如下。
func1
ADDr0,r0,r1
ADDr0,r0,r2
ADDr0,r0,r3
MOVpc,lr
caller1
MOVr3,#4
MOVr2,#3
MOVr1,#2
MOVr0,#1
Bfunc1
如果程序需要傳遞6個參數(shù),變?yōu)槿缦滦问健?/p>
intfunc2(inta,intb,intc,intd,inte,intf)
{
returna+b+c+d+e+f;
}
intcaller2(void)
{
returnfunc1(1,2,3,4,5,6);
}
則編譯后的匯編文件如下。
func2
STRlr,[sp,#-4]!
ADDr0,r0,r1
ADDr0,r0,r2
ADDr0,r0,r3
LDMIBsp,{r12,r14}
ADDr0,r0,r12
ADDr0,r0,r14
LDRpc,{sp},#4
caller2
STMFDsp!,{r2,r3,lr}
MOVr3,#6
MOVr2,#5
STMIAsp,{r2,r3}
MOVr3,#4
MOVr2,#3
MOVr1,#2
MOVr0,#1
BLfunc2
LDMFDsp!,{r2,r3,pc}
綜上所述,為了在程序中高效的調(diào)用函數(shù),最好遵循以下規(guī)則。
·盡量限制函數(shù)的參數(shù),不要超過4個,這樣函數(shù)調(diào)用的效率會更高。
·當傳遞的參數(shù)超過4個時,要將多個相關參數(shù)組織在一個結構體中,用傳遞結構體指針來代替多個參數(shù)。
·避免將傳遞的參數(shù)定義為longlong型,因為傳遞一個longlong型的數(shù)據(jù)將會占用兩個32位寄存器。
·函數(shù)中存在浮點運算時,避免使用double型參數(shù)。
14.9.2使用__value_in_regs返回結構體編譯選項__value_in_regs指示編譯器在整數(shù)寄存器中返回4個整數(shù)字的結構或者在浮點寄存器中返回4個浮點型或雙精度型值,而不使用存儲器。
下面的例子顯示了__value_in_regs選項的用法。
typedefstruct{inthi;uintlo;}int64;//注意該結構中,高位為有符號整數(shù),低位為無符號整數(shù)
__value_in_regsint64add64(int64x,int64y)
{int64res;
res.lo=x.lo+y.lo;
res.hi=x.hi+y.hi;
if(res.lo<y.lo)res.hi++;//carryfromlowword
returnres;
}
voidtest(void)
{int64a,b,c,sum;
a.hi=0x00000000;a.lo=0xF0000000;
b.hi=0x00000001;b.lo=0x10000001;
sum=add64(a,b);
c.hi=0x00000002;c.lo=0xFFFFFFFF;
sum=add64(sum,c);
}
編譯后的結果如下所示。
add64
ADDSa2,a2,a4
ADCa1,a3,a1
MOVpc,lr
test
STMDBsp!,{lr}
MOVa1,#0
MOVa2,#&f0000000
MOVa3,#1
MOVa4,#&10000001
BLadd64
MOVa3,#2
MVNa4,#0
LDMIAsp!,{lr}
Badd64
當使用__value_in_regs定義結構體時,編譯的代碼大小為52字節(jié),如果不使用__value_in_regs選項,則編譯出的結果為160字節(jié)(本書中沒有列出未使用__value_in_regs時的編譯結果,讀者有興趣可以自己上機試驗)。
14.9.3葉子函數(shù)所謂葉子函數(shù)(leaffunction)就是在其函數(shù)體內(nèi)不存在對其他函數(shù)調(diào)用,它也常被稱為終級函數(shù)。因為葉子函數(shù)不需要調(diào)用其他函數(shù),所有沒有保存/恢復寄存器的操作,因此執(zhí)行效率比一般函數(shù)要高。
當函數(shù)中必須對一些寄存器進行保存時,可以使用高效率的多寄存器存儲指令STM,對需要保存的寄存器內(nèi)存一次性存儲。
正是由于葉子函數(shù)執(zhí)行的高效性,所以在編程時,盡量將子程序編寫為葉子函數(shù),這樣即使程序中多次調(diào)用也不會影響代碼性能。
為了高效的調(diào)用函數(shù),可以遵循下面函數(shù)調(diào)用原則。
·避免在被頻繁調(diào)用的函數(shù)中調(diào)用其他函數(shù),以保證被頻繁調(diào)用的函數(shù)被編譯器編譯為葉子函數(shù)。
·把比較小的被調(diào)用函數(shù)和調(diào)用函數(shù)放在同一個源文件中,并且要先定義后調(diào)用,編譯器就可以優(yōu)化函數(shù)調(diào)用或內(nèi)聯(lián)較小的函數(shù)。
·對性能影響較大的重要函數(shù)可使用關鍵字_inline進行內(nèi)聯(lián)。
14.9.4嵌套優(yōu)化注意
嵌套優(yōu)化(Tail-Calloptimization)只適用于armcc。編譯時如果使用-g或-debug選項,編譯器自動關閉該功能。
一個函數(shù)如果在其結束時調(diào)用了另一個函數(shù),則編譯器使用B指令調(diào)轉到被調(diào)用函數(shù),而非BL指令。這樣就避免了一級不必要的函數(shù)返回。圖14.3顯示了嵌套優(yōu)化的調(diào)用過程。
圖14.3嵌套優(yōu)化函數(shù)調(diào)用過程
當編譯時使用-O1或-O2選項時,編譯器都執(zhí)行這種嵌套優(yōu)化。需要注意的是,當函數(shù)中引用了局部變量地址,由于指針別名問題的影響,即使函數(shù)在返回時調(diào)用了其他函數(shù),編譯器也不會使用嵌套優(yōu)化。
下面通過一個例子來分析嵌套優(yōu)化是如何提高代碼執(zhí)行效率的。
externintfunc2(int);
intfunc1(inta,intb)
{if(a>b)
return(func2(a-b));
else
return(func2(b-a));
}
編譯后的代碼如下所示。
func1
CMPa1,a2
SUBLEa1,a2,a1
SUBGTa1,a1,a2
Bfunc2
首先,func1中使用B指令代替BL指令,不用擔心lr寄存器被破壞,減少了對寄存器壓棧保護操作。另外,程序直接從func2返回到調(diào)用func1的函數(shù),減少一次函數(shù)返回。如果說正常的指令調(diào)用過程為:
BL+BL+MOVpc,lr+MOVpc,lr
那么經(jīng)過嵌套優(yōu)化的函數(shù)調(diào)用過程就可以表示為:
BL+BL+MOVpc,lr
這樣,總的開銷將減少25%。
14.9.5單純子函數(shù)所謂單純子函數(shù)(PureFunctions)是指那些函數(shù)返回值只和調(diào)用參數(shù)有關。換句話說,就是如果調(diào)用函數(shù)的參數(shù)相同,那么函數(shù)的返回結果也相同。如果程序中存在這樣的函數(shù),可以在函數(shù)定義時使用_pure進行聲明,這樣在程序編譯時編譯器會根據(jù)函數(shù)的調(diào)用情況對其進行優(yōu)化。
下面的例子顯示了當函數(shù)用_pure聲明時,編譯器對其所做的優(yōu)化。
程序源碼文件如下。
intsquare(intx)
{
returnx*x;
}
intf(intn)
{
returnsquare(n)+square(n)
}
編譯后的結果如下。
square
MOVa2,a1
MULa1,a2,a2
MOVpc,lr
f
STMDBsp!,{lr}
MOVa3,a1
BLsquare
MOVa4,a1
MOVa1,a3
BLsquare
ADDa1,a4,a1
LDMIAsp!,{pc}
上面的程序中,square函數(shù)為“單純子函數(shù)”,當使用_pure聲明該函數(shù)時編譯器在調(diào)用該函數(shù)時,將對程序進行優(yōu)化。
聲明的方法和編譯后的結果如下所示。
__pureintsquare(intx)
{
returnx*x;
}
f
STMDBsp!,{lr}
BLsquare
MOVa1,a1,LSL#1
LDMIAsp!,{pc}
從編譯后的代碼中可以看到,用_pure聲明的函數(shù)在f函數(shù)中只調(diào)用了一次。
雖然“單純子函數(shù)”可以提高代碼執(zhí)行效率,但同時也會帶來一些負面影響。比如,在“單純子函數(shù)”中,不能直接或間接訪問內(nèi)存地址。所以在程序中使用“單純子函數(shù)”時要特別小心。
另外,還可以使用#pragma聲明“單純子函數(shù)”,下面的代碼顯示了它的聲明過程。
#pragmano_side_effects
/*functiondefinition*/
#pragmaside_effects
14.9.6內(nèi)嵌函數(shù)ARM編譯器支持函數(shù)內(nèi)嵌功能。使用關鍵字“_inline”聲明函數(shù),可以使函數(shù)內(nèi)嵌。下面的例子顯示了如何使用函數(shù)內(nèi)嵌功能。
程序源文件如下。
__inlineintsquare(intx)
{
returnx*x;
}
#include<math.h>
doublelength(intx,inty)
{
returnsqrt(square(x)+square(y));
}
編譯結果如下所示。
length
STMDBsp!,{lr}
MULa3,a1,a1
MLAa1,a2,a2,a3
BL_dflt
LDMIAsp!,{lr}
Bsqrt
使用函數(shù)內(nèi)嵌有以下好處:
·減少了函數(shù)調(diào)用開銷(如寄存器的壓棧保護);
·減少了參數(shù)傳遞開銷;
·進一步提高了編譯器對代碼優(yōu)化的可能性(如編譯器可將ADD和MUL指令合并為一條MLA指令)。
但使用函數(shù)內(nèi)嵌將增加代碼尺寸。也正是處于這種原因,armcc和tcc都沒有提供函數(shù)自動內(nèi)嵌的編譯選項。
一般來說,只有對性能影響較大的重要函數(shù)才使用關鍵字_inline進行內(nèi)嵌。
14.9.7函數(shù)定義使用函數(shù)時要先定義后調(diào)用是ARM編程的基本規(guī)則之一。在函數(shù)調(diào)用之前定義函數(shù),編譯器可以檢查被調(diào)用函數(shù)的寄存器使用情況,從而對其進行進一步的優(yōu)化。
首先來看下面的例子。
intsquare(intx);
intsumsquares1(intx,inty)
{
returnsquare(x)+square(y);
}
/*square函數(shù)可以在本文件中定義,也可以在其他源文件中定義*/
intsquare(intx)
{
returnx*x;
}
intsumsquares2(intx,inty)
{
returnsquare(x)+square(y);
}
編譯的結果如下所示。
sumsquares1
STMDBsp!,{v1,v2,lr}
MOVv1,a2
BLsquare
MOVv2,a1
MOVa1,v1
BLsquare
ADDa1,v2,a1
LDMIAsp!,{v1,v2,pc}
square
MOVa2,a1
MULa1,a2,a2
MOVpc,lr
sumsquares2
STMDBsp!,{lr}
MOVa3,a2
BLsquare
MOVa4,a1
MOVa1,a3
BLsquare
ADDa1,a4,a1
LDMIAsp!,{pc}
從編譯的結果可以看出,將square函數(shù)定義放在sumsquares函數(shù)前,編譯器可以判斷寄存器a3和a4并未使用,所有在調(diào)用函數(shù)入口處并未將其壓棧。這樣,減少了內(nèi)存訪問,提高了代碼執(zhí)行效率。