A.6 控制流指令
分支与跳转行为的测量,较少依赖其他测量及具体应用,因此下面考察控制流指令的使用情况;它们与前几节的操作没有多少共同之处。
对于改变控制流的指令,术语并不统一。20 世纪 50 年代通常称为 transfer;从 1960 年起开始使用 branch,后来又出现其他名称。本书将无条件改变控制流的操作称为跳转(jump),将有条件改变控制流的操作称为分支(branch)。
控制流改变可分为四类:
- 条件分支;
- 跳转;
- 过程调用;
- 过程返回。
我们希望了解这些事件的相对频率,因为各事件不同,可能采用不同指令,也可能表现出不同行为。图 A.14 给出加载—存储计算机运行我们的基准程序时,各类控制流指令的频率。

图内术语:Call/return=调用/返回;Jump=跳转;Conditional branch=条件分支;Frequency of branch instructions=分支指令频率。浮点/整数程序平均值依次为 8%/19%、10%/6%、82%/75%。
控制流指令的寻址方式
控制流指令必须指定目标地址。绝大多数情况下,目标在指令中显式指定;过程返回是主要例外,因为返回目标在编译时未知。最常见的指定方式,是给出一个与程序计数器(PC)相加的位移,这类指令称为 PC 相对控制流指令。PC 相对分支或跳转有优势:目标通常距离当前指令很近,用相对于当前 PC 的位置表示,需要的位数更少。此外,PC 相对寻址还能让代码的运行不依赖装入位置。这种位置无关性可省去程序链接时的一部分工作,对于执行期间动态链接的程序也很有用。
要实现编译时目标未知的返回或间接跳转,就需要 PC 相对寻址以外的方法。必须能够动态指定目标,使它在运行时改变。最简单的方法,是指定一个包含目标地址的寄存器;也可以允许跳转使用任意寻址方式提供目标地址。
这种寄存器间接跳转还支持四项重要功能:
- 多数编程语言中的
case或switch语句,在多个选项之间选择。 - C++、Java 等面向对象语言中的虚函数或方法,根据参数类型调用不同例程。
- C、C++ 等语言中的高阶函数或函数指针,允许将函数作为参数传递,带来某些类似面向对象编程的能力。
- 动态共享库:只有程序实际调用时,才在运行期间装入和链接库,而非在运行前静态装入和链接。
以上四种情况下,目标地址在编译时都未知,因此通常先从存储器装入寄存器,再执行寄存器间接跳转。
分支通常通过 PC 相对寻址指定目标,因此一个重要问题是:目标距离分支有多远?了解位移分布,有助于选择支持的分支偏移范围,从而影响指令长度与编码。图 A.15 给出以指令条数表示的 PC 相对分支位移分布。约 75% 的分支是前向分支。

图内术语:Percentage of distance=各距离占比;Bits of branch displacement=分支位移位数;Integer average=整数程序平均值;Floating-point average=浮点程序平均值。
条件分支的选择
多数控制流改变属于分支,因此如何指定分支条件是一项重要决定。图 A.16 给出目前使用的三种主要方法及其优缺点。
| 名称 | 实例 | 条件测试方法 | 优点 | 缺点 |
|---|---|---|---|---|
| 条件码(CC) | 80x86、ARM、PowerPC、SPARC、SuperH | 测试 ALU 操作设置的专用位;是否设置也可能由程序控制。 | 有时可顺便得到条件,无需额外代价。 | 条件码构成额外状态;由于它在指令与分支之间传递信息,会限制指令重排。 |
| 条件寄存器/有限比较 | Alpha、MIPS | 用简单比较(相等或与零比较)的结果,测试任意寄存器。 | 简单。 | 比较能力有限,可能影响关键路径;一般条件可能需要额外比较。 |
| 比较并分支 | PA-RISC、VAX、RISC-V | 比较是分支的一部分,允许较通用的比较,如大于、小于。 | 一个分支只需一条指令,而非两条。 | 可能决定分支指令的关键路径。 |
分支一个很显著的特点,是大量比较都是简单测试,其中许多是与零比较。因此,一些体系结构将它们作为特殊情况处理,尤其是在采用比较并分支指令时。图 A.17 给出条件分支使用各种比较的频率。

图内术语:Not equal=不等于;Equal=等于;Greater than or equal=大于或等于;Greater than=大于;Less than or equal=小于或等于;Less than=小于。依次对应的浮点/整数程序平均值为 5%/2%、16%/18%、0%/11%、0%/0%、44%/33%、34%/35%。横轴为分支中各比较类型的频率。
过程调用的选择
过程调用与返回包含控制转移,也可能保存部分状态;至少要将返回地址保存在某处,有时放在专用链接寄存器中,也可以直接使用 GPR。一些较早的体系结构提供一次保存许多寄存器的机制,而较新体系结构要求编译器为每一个需要保存、恢复的寄存器生成存储和加载指令。
保存寄存器有两种基本约定:在调用处保存,或在被调用过程内部保存。调用者保存(caller saving)要求调用过程保存调用后还要使用的寄存器,被调用过程无需顾虑它们;被调用者保存(callee saving)则相反,被调用过程保存自己准备使用的寄存器,使调用者不受约束。有时,由于两个过程访问全局可见变量的方式,必须采用调用者保存。例如,过程 P1 调用 P2,两个过程都会操作全局变量 x。若 P1 将 x 分配到寄存器,就必须在调用 P2 前,将 x 保存到 P2 知道的位置。独立编译的可能性,会增加编译器判断被调用过程是否可能访问这些寄存器变量的难度。假定 P2 本身不访问 x,但可能调用访问 x 的 P3,而 P2、P3 又是分别编译的。由于这些复杂性,多数编译器会保守地在调用者一侧保存任何可能在调用期间被访问的变量。
两种约定都可用时,有些程序采用被调用者保存更好,另一些采用调用者保存更好。因此,当今多数实际系统混合使用两种机制。应用二进制接口(ABI)规定这类约定,包括哪些寄存器由调用者保存、哪些由被调用者保存。本附录后面将考察,复杂的自动寄存器保存指令为什么可能不适合编译器的需要。
小结:控制流指令
控制流指令是执行最频繁的指令之一。条件分支虽有多种选择,但一种新体系结构的分支寻址,应当能够到达分支前后数百条指令范围内的目标。这意味着 PC 相对分支位移至少需要 8 位。跳转指令还应支持寄存器间接和 PC 相对寻址,以支持返回及当今系统的许多其他功能。
到这里,我们已经从汇编程序员或编译器作者的视角,完成了对指令体系结构的考察。倾向的方案是加载—存储体系结构,支持位移、立即数、寄存器间接寻址;数据包括 8、16、32、64 位整数,以及 32、64 位浮点数;指令包括简单操作、PC 相对条件分支、用于过程调用的跳转并链接指令,以及用于过程返回等用途的寄存器间接跳转。
接下来要选择如何表示这种体系结构,以便硬件高效执行。