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