高效的C編程之:寄存器分配
編譯器一項(xiàng)很重要的優(yōu)化功能就是對(duì)寄存器的分配。與分配在寄存器中的變量相比,分配到內(nèi)存的變量訪問要慢得多。所以如何將盡可能多的變量分配到寄存器,是編程時(shí)應(yīng)該重點(diǎn)考慮的問題。
注意
當(dāng)使用-g或-dubug選項(xiàng)編譯程序時(shí),為了確保調(diào)試信息的完整性,寄存器分配的效率比不使用-g或-dubug選項(xiàng)低很多。
14.7.1變量寄存器分配一般情況下,編譯器會(huì)對(duì)C函數(shù)中的每一個(gè)局部變量分配一個(gè)寄存器。如果多個(gè)局部變量不會(huì)交迭使用,那么編譯器會(huì)對(duì)它們分配同一個(gè)寄存器。當(dāng)局部變量多于可用的寄存器時(shí),編譯器會(huì)把多余的變量存儲(chǔ)到堆棧。這些被寫入堆棧需要訪問存儲(chǔ)器的變量被稱為溢出(Spilled)變量。
為了提高程序的執(zhí)行效率:
·使溢出變量的數(shù)量最少;
·確保最重要的和經(jīng)常用到的變量被分配在寄存器中。
可以被分配到寄存器的變量包括:
·程序中的局部變量;
·調(diào)用子程序時(shí)傳遞的參數(shù);
·與地址無關(guān)變量。
另外,在一些特定條件下,結(jié)構(gòu)體中的域也可以被分配到寄存器中。
表14.1顯示了當(dāng)C編譯器采用ARM-Thumb過程調(diào)用標(biāo)準(zhǔn)時(shí),內(nèi)部寄存器的編號(hào)、名字和分配方法。
表14.1 C編譯器寄存器用法
寄存器編號(hào)
可選寄存器名
特殊寄存器名
寄存器用法
r0
a1
函數(shù)調(diào)用時(shí)的參數(shù)寄存器,用來存放前4個(gè)函數(shù)參數(shù)和存放返回值。在函數(shù)內(nèi)如果將這些寄存器用作其他用途,將破壞其值。
r1
a2
r2
a3
r3
a4
r4
v1
通用變量寄存器
r5
v2
r6
v3
r7
v4
r8
v5
r9
v6或SB或TR
平臺(tái)寄存器,不同的平臺(tái)對(duì)該寄存器的定義不同
r10
v7
通用變量寄存器。在使用堆棧邊界檢測的情況下,r10保存堆棧邊界的地址
r11
v8
通用變量寄存器。
r12
IP
臨時(shí)過渡寄存器,函數(shù)調(diào)用時(shí)會(huì)破壞其中的值
r13
SP
堆棧指針
r14
LR
鏈接寄存器
r15
PC
程序計(jì)數(shù)器
從表14.1可以看出,編譯器可以分配14個(gè)變量到寄存器而不會(huì)發(fā)生溢出。但有些寄存器編譯器會(huì)有特殊用途(如r12),所以在編寫程序時(shí)應(yīng)盡量限制變量的數(shù)目,使函數(shù)內(nèi)部最多使用12個(gè)寄存器。
注意
在C語言中,可以使用關(guān)鍵詞register給指定變量分配專用寄存器。但不同的編譯器對(duì)該關(guān)鍵詞的處理可能不同,使用時(shí)要查閱相關(guān)手冊。
14.7.2指針別名C語言中的指針變量可以給編程帶來很大的方便。但使用指針變量時(shí)要特別小心,它很可能使程序的執(zhí)行效率下降。在一個(gè)函數(shù)中,編譯器通常不知道是否有2個(gè)或2個(gè)以上的指針指向同一個(gè)地址對(duì)象。所以編譯器認(rèn)為,對(duì)任何一個(gè)指針的寫入都將會(huì)影響從任何其他指針的讀出,但這樣會(huì)明顯降低代碼執(zhí)行的效率。這就是著名的“寄存器別名(PointerAliasing)”問題。
注意
一些編譯器提供了“忽略指針別名”選項(xiàng),但這可能給程序帶來潛在的bug。ARM編譯器是遵循ANSI/ISO標(biāo)準(zhǔn)的編譯器,不提供該選項(xiàng)。
1.局部變量指針別名問題通常情況下,編譯器會(huì)試圖對(duì)C函數(shù)中的每一個(gè)局部變量分配一個(gè)寄存器。但當(dāng)局部變量是指向內(nèi)存地址的指針時(shí),情況有所不同。先來看一個(gè)簡單的例子。
voidadd(int*i)
{
inttotal1=0,total2=0;
total1+=*i;
total2+=*i;
}
編譯后生成:
add:
0000807CE3A01000MOVr1,#0
>>>POINTALIAS\#3inttotal1=0,total2=0;
00008080E3A02000MOVr2,#0
>>>POINTALIAS\#5total1+=*i;
00008084E5903000LDRr3,[r0,#0]
00008088E0831001ADDr1,r3,r1
>>>POINTALIAS\#6total2+=*i;
0000808CE5903000LDRr3,[r0,#0]
00008090E0832002ADDr2,r3,r2
>>>POINTALIAS\#8}
00008094E12FFF1EBXr14
>>>POINTALIAS\#11{
注意程序中i的值被裝載了兩次。因?yàn)榫幾g器不能確定指針*i是否有別名存在,這就使得編譯器不得不增加一條額外的Load指令。
另一個(gè)問題,當(dāng)在函數(shù)中要獲得局部變量地址時(shí),這個(gè)變量就被一個(gè)指針?biāo)鶎?duì)應(yīng),就可能與其他指針產(chǎn)生別名。為了防止別名發(fā)生,在每次對(duì)變量操作時(shí),編譯器就會(huì)從堆棧中重新讀入數(shù)據(jù)。考慮下面的例子程序,分析其產(chǎn)生的編譯結(jié)果。
voidf(int*a);
intg(inta);
inttest1(inti)
{f(&i);
/*nowuse’i’extensively*/
i+=g(i);
i+=g(i);
returni;
}
編譯結(jié)果如下所示。
test1
STMDBsp!,{a1,lr}
MOVa1,sp
BLf
LDRa1,[sp,#0]
BLg
LDRa2,[sp,#0]
ADDa1,a1,a2
STRa1,[sp,#0]
BLg
LDRa2,[sp,#0]
ADDa1,a1,a2
ADDsp,sp,#4
LDMIAsp!,{pc}
從上面代碼的編譯結(jié)果可以看出,對(duì)每一次i操作,編譯器都將會(huì)從堆棧中讀出其值。這是因?yàn)?,一旦在函?shù)中出現(xiàn)對(duì)i的取值操作,編譯器就會(huì)擔(dān)心別名問題。為了避免這種情況,盡量不要在程序中使用局部變量地址。如果必須這么做,那么可以在使用之前先把局部變量的值復(fù)制到另外一個(gè)局部變量中。下面的程序是對(duì)test1函數(shù)的優(yōu)化。
inttest2(inti)
{
intdummy=i;
f(&dummy);
i=dummy;
/*nowuse’i’extensively*/
i+=g(i);
i+=g(i);
returni;
}
編譯后的結(jié)果如下。
test2
STMDBsp!,{v1,lr}
STRa1,[sp,#-4]!
MOVa1,sp
BLf
LDRv1,[sp,#0]
MOVa1,v1
BLg
ADDv1,a1,v1
MOVa1,v1
BLg
ADDa1,a1,v1
ADDsp,sp,#4
LDMIAsp!,{v1,pc}
從編譯結(jié)果可以看出,修改后的代碼只使用了2次內(nèi)存訪問,而test1為4次內(nèi)存訪問。
總上所述,為了在程序中避免指針別名,應(yīng)該做到:
·避免使用局部變量地址;
·如果程序中出現(xiàn)多次對(duì)同一指針的訪問,應(yīng)先將其值取出并保存到臨時(shí)變量中。
2.全局變量通常情況下,編譯器不會(huì)為全局變量分配寄存器。這樣在程序中使用全局變量,很可能帶來內(nèi)存訪問上的開銷。所有盡量避免在循環(huán)體內(nèi)使用全局變量,以減少對(duì)內(nèi)存的訪問次數(shù)。
如果在一段程序體內(nèi)大量使用了同一個(gè)全局變量,建議在使用前先將其拷貝到一個(gè)局部的臨時(shí)變量中,當(dāng)完成對(duì)它的全部操作后,再將其寫回到內(nèi)存。
比較下面兩個(gè)完成同樣功能的函數(shù),分析全局變量的操作對(duì)程序性能的影響。
intf(void);
intg(void);
interrs;
voidtest1(void)
{
errs+=f();
errs+=g();
}
voidtest2(void)
{
intlocalerrs=errs;
localerrs+=f();
localerrs+=g();
errs=localerrs;
}
編譯結(jié)果如下。
test1
STMDBsp!,{v1,lr}
BLf
LDRv1,[pc,#L00002c-.-8]
LDRa2,[v1,#0]
ADDa1,a1,a2
STRa1,[v1,#0]
BLg
LDRa2,[v1,#0]
ADDa1,a1,a2
STRa1,[v1,#0]
LDMIAsp!,{v1,pc}
L00002c
DCD|x$dataseg|
test2
STMDBsp!,{v1,v2,lr}
LDRv1,[pc,#L00002c-.-8]
LDRv2,[v1,#0]
BLf
ADDv2,a1,v2
BLg
ADDa1,a1,v2
STRa1,[v1,#0]
LDMIAsp!,{v1,v2,pc}
從編譯的結(jié)果中可以看出,test1中每次對(duì)全局變量errs的訪問都會(huì)使用耗時(shí)的Load/Store指令;而test2只使用了一次內(nèi)存訪問指令。這對(duì)提高程序的整體性能有很大幫助。
3.指針鏈指針鏈(PointerChains)常被用來訪問結(jié)構(gòu)體內(nèi)部變量。下面的例子顯示了一個(gè)典型的指針鏈的使用。
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;
}
上面的代碼每次使用“p->pos”時(shí)都會(huì)對(duì)變量重新取值。為了提高代碼效率,將程序改寫如下。
voidInitPos2(Object*p)
{
Point3*pos=p->pos;
pos->x=0;
pos->y=0;
pos->z=0;
}
經(jīng)過改寫的代碼,減少了內(nèi)存訪問次數(shù),提高程序的執(zhí)行效率,另外也可以在object結(jié)構(gòu)體中增加一個(gè)point3域,專門作為指向p->pos的指針。