|  | 
 
| 14.7  寄存器分配 
 编译器一项很重要的优化功能就是对寄存器的分配。与分配在寄存器中的变量相比,分配到内存的变量访问要慢得多。所以如何将尽可能多的变量分配到寄存器,是编程时应该重点考虑的问题。
 
 
 | 
 | 注意 
 | 当使用-g或-dubug选项编译程序时,为了确保调试信息的完整性,寄存器分配的效率比不使用-g或-dubug选项低很多。 
 | 
 
 14.7.1  变量寄存器分配
 
 一般情况下,编译器会对C函数中的每一个局部变量分配一个寄存器。如果多个局部变量不会交迭使用,那么编译器会对它们分配同一个寄存器。当局部变量多于可用的寄存器时,编译器会把多余的变量存储到堆栈。这些被写入堆栈需要访问存储器的变量被称为溢出(Spilled)变量。
 
 为了提高程序的执行效率:
 ·  使溢出变量的数量最少;
 ·  确保最重要的和经常用到的变量被分配在寄存器中。
 
 可以被分配到寄存器的变量包括:
 ·  程序中的局部变量;
 ·  调用子程序时传递的参数;
 ·  与地址无关变量。
 
 另外,在一些特定条件下,结构体中的域也可以被分配到寄存器中。
 
 表14.1显示了当C编译器采用ARM-Thumb过程调用标准时,内部寄存器的编号、名字和分配方法。
 表14.1 C编译器寄存器用法
 
 | 寄存器编号 
 | 可选寄存器名 
 | 特殊寄存器名 
 | 寄存器用法 
 |  | r0 
 | a1 
 | 
 | 函数调用时的参数寄存器,用来存放前4个函数参数和存放返回值。在函数内如果将这些寄存器用作其他用途,将破坏其值。 
 |  | 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 
 | 临时过渡寄存器,函数调用时会破坏其中的值 
 |  | r13 
 | 
 | SP 
 | 堆栈指针 
 |  | r14 
 | 
 | LR 
 | 链接寄存器 
 |  | r15 
 | 
 | PC 
 | 程序计数器 
 | 
 从表14.1可以看出,编译器可以分配14个变量到寄存器而不会发生溢出。但有些寄存器编译器会有特殊用途(如r12),所以在编写程序时应尽量限制变量的数目,使函数内部最多使用12个寄存器。
 
 
 | 
 | 注意 
 | 在C语言中,可以使用关键词register给指定变量分配专用寄存器。但不同的编译器对该关键词的处理可能不同,使用时要查阅相关手册。 
 | 
 14.7.2  指针别名
 
 C语言中的指针变量可以给编程带来很大的方便。但使用指针变量时要特别小心,它很可能使程序的执行效率下降。在一个函数中,编译器通常不知道是否有2个或2个以上的指针指向同一个地址对象。所以编译器认为,对任何一个指针的写入都将会影响从任何其他指针的读出,但这样会明显降低代码执行的效率。这就是著名的“寄存器别名(Pointer Aliasing)”问题。
 
 
 | 
 | 注意 
 | 一些编译器提供了“忽略指针别名”选项,但这可能给程序带来潜在的bug。ARM编译器是遵循ANSI/ISO标准的编译器,不提供该选项。 
 | 
 1.局部变量指针别名问题
 
 通常情况下,编译器会试图对C函数中的每一个局部变量分配一个寄存器。但当局部变量是指向内存地址的指针时,情况有所不同。先来看一个简单的例子。
 
 void add(int * i)
 {
 int total1=0,total2=0;
 
 total1+= *i;
 total2+= *i;
 
 }
 
 编译后生成:
 
 add:
 0000807C E3A01000  MOV      r1,#0
 >>> POINTALIAS\#3         int total1=0,total2=0;
 00008080 E3A02000  MOV      r2,#0
 >>> POINTALIAS\#5         total1+= *i;
 00008084 E5903000  LDR      r3,[r0,#0]
 00008088 E0831001  ADD      r1,r3,r1
 >>> POINTALIAS\#6         total2+= *i;
 0000808C E5903000  LDR      r3,[r0,#0]
 00008090 E0832002  ADD      r2,r3,r2
 >>> POINTALIAS\#8 }
 00008094 E12FFF1E  BX       r14
 >>> POINTALIAS\#11 {
 
 注意程序中i的值被装载了两次。因为编译器不能确定指针*i是否有别名存在,这就使得编译器不得不增加一条额外的Load指令。
 
 另一个问题,当在函数中要获得局部变量地址时,这个变量就被一个指针所对应,就可能与其他指针产生别名。为了防止别名发生,在每次对变量操作时,编译器就会从堆栈中重新读入数据。考虑下面的例子程序,分析其产生的编译结果。
 
 void f(int *a);
 int g(int a);
 int test1(int i)
 { f(&i);
 /* now use ’i’ extensively */
 i += g(i);
 i += g(i);
 return i;
 }
 
 编译结果如下所示。
 
 test1
 STMDB    sp!,{a1,lr}
 MOV      a1,sp
 BL       f
 LDR      a1,[sp,#0]
 BL       g
 LDR      a2,[sp,#0]
 ADD      a1,a1,a2
 STR      a1,[sp,#0]
 BL       g
 LDR      a2,[sp,#0]
 ADD      a1,a1,a2
 ADD      sp,sp,#4
 LDMIA    sp!,{pc}
 
 从上面代码的编译结果可以看出,对每一次i操作,编译器都将会从堆栈中读出其值。这是因为,一旦在函数中出现对i的取值操作,编译器就会担心别名问题。为了避免这种情况,尽量不要在程序中使用局部变量地址。如果必须这么做,那么可以在使用之前先把局部变量的值复制到另外一个局部变量中。下面的程序是对test1函数的优化。
 
 int test2(int i)
 {
 int dummy = i;
 f(&dummy);
 i = dummy;
 /* now use ’i’ extensively */
 i += g(i);
 i += g(i);
 return i;
 }
 
 编译后的结果如下。
 
 test2
 STMDB    sp!,{v1,lr}
 STR      a1,[sp,#-4]!
 MOV      a1,sp
 BL       f
 LDR      v1,[sp,#0]
 MOV      a1,v1
 BL       g
 ADD      v1,a1,v1
 MOV      a1,v1
 BL       g
 ADD      a1,a1,v1
 ADD      sp,sp,#4
 LDMIA    sp!,{v1,pc}
 
 从编译结果可以看出,修改后的代码只使用了2次内存访问,而test1为4次内存访问。
 
 总上所述,为了在程序中避免指针别名,应该做到:
 ·  避免使用局部变量地址;
 ·  如果程序中出现多次对同一指针的访问,应先将其值取出并保存到临时变量中。
 
 2.全局变量
 
 通常情况下,编译器不会为全局变量分配寄存器。这样在程序中使用全局变量,很可能带来内存访问上的开销。所有尽量避免在循环体内使用全局变量,以减少对内存的访问次数。
 
 如果在一段程序体内大量使用了同一个全局变量,建议在使用前先将其拷贝到一个局部的临时变量中,当完成对它的全部操作后,再将其写回到内存。
 
 比较下面两个完成同样功能的函数,分析全局变量的操作对程序性能的影响。
 
 int f(void);
 int g(void);
 int errs;
 void test1(void)
 {
 errs += f();
 errs += g();
 }
 void test2(void)
 {
 int localerrs = errs;
 localerrs += f();
 localerrs += g();
 errs = localerrs;
 }
 
 编译结果如下。
 
 test1
 STMDB    sp!,{v1,lr}
 BL       f
 LDR      v1,[pc, #L00002c-.-8]
 LDR      a2,[v1,#0]
 ADD      a1,a1,a2
 STR      a1,[v1,#0]
 BL       g
 LDR      a2,[v1,#0]
 ADD      a1,a1,a2
 STR      a1,[v1,#0]
 LDMIA    sp!,{v1,pc}
 L00002c
 DCD      |x$dataseg|
 test2
 STMDB    sp!,{v1,v2,lr}
 LDR      v1,[pc, #L00002c-.-8]
 LDR      v2,[v1,#0]
 BL       f
 ADD      v2,a1,v2
 BL       g
 ADD      a1,a1,v2
 STR      a1,[v1,#0]
 LDMIA    sp!,{v1,v2,pc}
 
 从编译的结果中可以看出,test1中每次对全局变量errs的访问都会使用耗时的Load/Store指令;而test2只使用了一次内存访问指令。这对提高程序的整体性能有很大帮助。
 
 3.指针链
 
 指针链(Pointer Chains)常被用来访问结构体内部变量。下面的例子显示了一个典型的指针链的使用。
 
 typedef struct { int x, y, z; } Point3;
 typedef struct { Point3 *pos, *direction; } Object;
 void InitPos1(Object *p)
 {
 p->pos->x = 0;
 p->pos->y = 0;
 p->pos->z = 0;
 }
 
 上面的代码每次使用“p->pos”时都会对变量重新取值。为了提高代码效率,将程序改写如下。
 
 void InitPos2(Object *p)
 {
 Point3 *pos = p->pos;
 pos->x = 0;
 pos->y = 0;
 pos->z = 0;
 }
 经过改写的代码,减少了内存访问次数,提高程序的执行效率,另外也可以在object结构体中增加一个point3域,专门作为指向p->pos的指针。
 | 
 |