指令集体系结构----程序员或编译器编写人员能够看到的计算机部分。
指令集体系结构包括:
1.对各种指令集进行了分类,并对各种方法的优势和劣势进行某种量化评估。
2.对一些指令集测量数据进行分析。
3.语言与编译器问题以及他们对指令集体系结构的影响。
桌面式计算机强调设计整数、浮点类型的程序的性能,很少考虑程序的规模。今天的服务器主要用于数据库、文件服务器和Web应用程序,再加上一些正对许多用户的时分应用程序。因此浮点性能的重要性要远低于整数与字符字符串的重要性,但是所有服务器处理器都仍包含浮点指令。
个人移动设备和嵌入式应用看重成本和能耗,所以代码规模非常重要,因为减少存储器可以降低价格和能耗,为了降低芯片成本,某些类型的指令称为可选选项。
由于与PC软件保持二进制兼容的重要性,再加上摩尔定律提供了充足的晶体管,使Intel在内部使用RISC指令集,而在外部支持80x86指令集。
后来的80x86微处理器使用硬件将80x86指令转换为类RISC的指令,然后在芯片内部执行经过转换的操作。它们仍然向程序员展现80x86体系结构,同时允许计算机设计人员实现RISC类型的处理器,以提高性能。
指令集体系结构的分类
处理器中的内部存储类型是最基本的区别,主要包括栈、累加器或寄存器组。操作数可以显式命名,也可以隐式命名。
在栈体系结构中,操作数隐式位于栈的顶部,而在累加器体系结构中,操作数隐式为累加器。
通用寄存器体系结构只有显式操作数,或者为寄存器,或者为存储器位置。
四类指令集中C=A+B的代码序列如下表:
栈
|
累加器
|
寄存器(寄存器--存储器)
|
寄存器(载入--存储)
|
Push A
|
Load A
|
Load R1,A
|
Load R1,A
|
Push B
|
Add B
|
Add R3,R1,B
|
Load R2,B
|
Add
|
Store C
|
Store R3,C
|
Add R3,R1,R2
|
Pop C
|
|
|
Store R3,c
|
对于栈和累加器体系结构,Add指令拥有隐式操作数,对于寄存器体系结构拥有显示操作数。如上表所示,实际上有两种类型的寄存器计算机。一类可以用任意指令来访问存储器,称为寄存器--存储器体系结构,另一类则只能用载入和存储指令访问存储器,称为载入--存储体系结构。第三类将所有操作数都保存在存储器中,称为存储器--存储器体系结构。一些指令集体系结构的寄存器要多于单个累加器,但对这些特殊寄存器的使用设置一些限制,称为拓展累加器或专用寄存器计算机。
尽管大多数早期计算机使用栈或累加器类型的体系结构,但是新体系结构都使用了载入--存储寄存器体系结构。通用寄存器计算机之所以出现,主要原因是1.寄存器快于存储器 2.对编译器来说,使用寄存器要比使用其他内部存储形式的效率更高。
例如在寄存器计算机中,在对表达式(AxB)-(BxC)-(AxD)求值时,可以按任意顺序执行乘法计算,这种做法的效率更高,可能是操作数位置的原因,也可能是流水线因素的原因。
在栈计算机上,硬件只能按唯一的顺序对表达式进行求值,因为操作数隐藏在栈中的,它必须多次载入操作数。
寄存器可用于保存变量,当变量被分配到寄存器中时,可以降低存储器通信流量、加快程序速度,提高代码密度(由于存储器的名称位数少于存储器位置的名称位数)。
编译器编写人员希望所有人员希望所有寄存器都是等价的。较早的计算机将一些寄存器专门用于一些特殊应用,显著降低了通用寄存器的数量。编译器将所有未确认用途的寄存器保留给表达式求值使用。
大多数编译器会为表达式求值保留一些寄存器,为参数传递使用一些,其余寄存器可用于保存变量。现代编译器技术能够有效地使用大量寄存器,从而增加最新体系结构的寄存器数目。
有两个重要指令集特性可以区分GPR(通用寄存器)体系结构,这两个特性都关注一个典型算术或逻辑指令操作的本质,第一个特性关注一个ALU指令是有两个还是三个操作数。
在三操作数格式中,指令包含一个结果操作数和两个源操作数。在两操作数格式中,操作数之一即是结果操作数也是源操作数。
GPR体系结构的第二个区别是考虑ALU指令中可能有多少个操作数是存储器地址。一个典型ALU指令支持的存储器操作数数量可能是0-3个。
存储器地址的数目 | 所允许的最大操作数个数 | 体系结构的类型 | 示例 |
0 | 3 | 载入-存储 | Alpha、ARM、MIPS、PowerPC、SPARC |
1 | 2 | 寄存器--存储器 | IBM 360/370 Intel80x86 |
2 | 2 | 存储器--存储器 | VAX |
3 | 3 | 存储器--存储器 | VAX |
类型 | 优势 | 劣势 |
寄存器--寄存器
(0,3)
|
简单、固定长度指令编码。简单代码生成模型。
指令执行所需要的时钟数相同。
|
指令数目高于指令中有存储器引用的体系结构。
指令多、指令密度低,增大了程序的规模。
|
寄存器--存储器
(1,2)
|
无需独立的载入指令就可以访问数据。
指令格式易于编码,可以得到很好的指令密度。
|
由于在二元运算中,源操作数会被销毁,所以操作数是不等价的。
在每条指令中对寄存器数目和存储器地址进行编码可能会限制寄存器的个数。
每条指令的时钟数会随操作数的位置变化。
|
存储器--存储器
(2,2)或(3,3)
|
最紧凑,没有为临时值浪费寄存器 | 指令规模变化很大,特别是对于三操作数指令。存储器访问会造成存储器瓶颈。 |
存储器寻址
一个体系结构,无论是载入--存储式,还是允许任何操作数都是存储器引用,都必须定义如何解释存储器地址以及如何指定这些地址。
1.解释存储器地址
大部分指令集都是字节寻址的,提供对字节、半字和字的访问方式。大部分计算机还提供双字的访问。小端字节顺序将地址为"x..x000"的字节放在双字的最低有效位置(小端)。
大端字节顺序将地址为"x...x000"的字节放在双字的最高有效位置(大端)。
在对比字符串时,小端排序不能与字的正常排序方式相匹配,字符串在寄存器中是反向表示的。
在许多计算机中,对大于一字节的对象进行寻址时都必须是对齐的。大小为s字节的对象,字节地址为A,如果A mod s = 0 ,则对该对象的寻址是对齐的。
由于存储器通常与一个字或双字的倍数边界对齐,所以非对齐寻址会增加硬件复杂性。一个非对齐存储器寻址可能需要多个对齐的存储器引用,因此即使在允许非对齐寻址的计算机中,采用对齐寻址的程序也可以运行更快。
即使数据是对齐的,要支持字节、半字和字寻址也需要一个对齐网络来对齐64位寄存器中的字节、半字和字。
2.寻址方式
除了存储器中的位置之外,寻址方式还指定常量和寄存器。在使用存储器位置时,由寻址方式指定的实际存储器地址称为有效地址。
寻址方式 | 指令举例 | 含义 | 使用场景 |
寄存器寻址 | Add R4,R3 | Regs[R4] <-- Regs[R4] + Regs[R3] | 当一个值在寄存器中 |
立即数寻址 | Add R4,#3 | Regs[R4] <-- Regs[R4] + 3 | 对于常量 |
位移量寻址 | Add R4,100(R1) | Regs[R4] <-- Regs[R4] + Mem[100 + Regs[R1]] | 访问本地变量(+模拟寄存器间接、直接寻址方式) |
寄存器间接寻址 | Add R4,(R1) | Regs[R4] <-- Regs[R4] + Mem[Regs[R1]] | 使用指针或计算得出的地址寻址 |
索引寻址 | Add R3,(R1 + R2) | Regs[R3] <-- Regs[R3] + Mem[Regs[R1]+[R2]] | 有时用于数组寻址:R1为数组基址,R2为索引值 |
直接或间接寻址 | Add R1,(1001) | Regs[R1] <-- Regs[R1] + Mem[1001] | 有时用于访问静态数据,地址常量可能很大 |
存储器间接寻址 | Add R1,@(R3) | Regs[R1] <-- Regs[R1]+Mem[Mem[Regs[R3]]] | 如果R3为指针p的地址,则此方法生成*p |
自动递增寻址 | Add R1,(R2)+ |
Regs[R1] <-- Regs[R1] + Mem[Regs[R2]]
Regs[R2] <-- Regs[R2] + d
|
用于在循环内部逐步遍历数组。R2为元素的开始位置,每次引用都会将R2增加一个元素d的值 |
自动递减寻址 | Add R1,-(R2) |
Regs[R2] <-- Regs[R2] -d
Regs[R1] <-- Regs[R1] + Mem[Regs[R2]]
|
与自动递增的用途相同,自动递增或递减也可以用作push/pop,以实现栈 |
比例寻址 | Add R1,100(R2)[R3] | Regs[R1] <-- Regs[R1] + Mem[100 + Regs[R2] + Regs[R3]*d] | 用于索引数组,在某些计算机中,可用于任何索引寻址方式 |
寻址模式能够大幅减少指令数目,他们也会增加构建计算机的复杂度,对于实施寻址模式的计算机,可能会增加每条指令的平均时钟周期数目(CPI)。在这些寻址方式中,位移量寻址和立即数寻址成为主导寻址方式。
位移量寻址方式
在使用位移量类型的寻址方式时,一个主要问题就是所用位移量的范围。由于位移量字段的大小直接影响到指令的长度,所以其选择非常重要。
位移量的分布非常广泛,既存在大量小数值,又有相当数量的大数值。位移值的广泛分布是由于变量有多个存储区域,而且访问他们的位移量不同,并且编译器使用的总寻址机制也各有不同。
大多数位移值是正数,但最大的胃一直为负数(14位以上)。位移量的位数--整数平均值在0-1之间,浮点平均值为13左右。
立即数或直接操作数寻址方式
在进行算术运算、比较(主要用于分支)和移动时,如果希望将常量放在寄存器中,可以使用立即数。
直接操作数寻址方式可用于写在代码中的常量和地址常量。对于立即数的使用,重点是要知道是需要对所有运算都支持立即数还是仅对一部分运算支持立即数。
大约有四分之一的数据传送和ALU运算拥有立即操作数。整数程序在大约五分之一的指令中使用立即数,而浮点程序在大约六分之一的指令中使用立即数。
对于载入操作,载入立即数指令将16位载入一个32位寄存器的任一半中,载入立即数并不是严格意义上的载入,因为他们并不访问存储器。
与位移值相似,立即数取值的大小也影响到指令长度,小立即数的应用最多,有时也会使用大型立即数,更多的是用在地址计算中。
当预测一种新的体系结构时至少支持以下寻址方式:位移量寻址、立即数寻址和寄存器间接寻址。位移量寻址方式中的地址大小至少为12~16位,立即数字段的大小至少为8~16位。
操作数的类型与大小
常见的操作数类型包括字符(8位)、半字(16位)、字(32位)、单精度浮点(1个字)和双精度浮点(2个字)。整数都是用二进制补码数字表示的,所有计算机都遵循相同的浮点标准---IEEE标准754.
一些体系结构提供了对字符串的操作,但是这些操作十分优先,将字符串中的每个字符都看作单个字符,支持对字符串执行的典型操作包括比较和移动。
一些体系结构支持二进制格式,通常称为压缩十进制会二进制编码十进制,通常提供在被称为压缩和解压缩的操作之间来回转换。
对于基准测试程序,所访问数据的大小分布,如下图:
双字数据类型用于表示浮点程序中的双精度浮点值,还用于表示地址,这是因为该计算机使用64位地址。在采用32位地址的计算机上,64位地址将被32位地址替代,所以整数程序中的几乎所有双字访问都会变成单字访问。
在一些体系结构中,寄存器中的对象可以作为字节或半字进行访问,这种访问非常少见。
指令集中的操作
下表给出10种简单指令,对于一组在8086上运行的整数程序,这10种简单指令占到所执行指令的96%。也就是说,执行最多的指令是一个指令集中的简单操作。
指令操作符的分类与示例,所有计算机通常都提供所有前三类运算
操作符类型 | 实例 |
算术与逻辑 | 整数算术与逻辑运算:加减乘除与或 |
数据传送 | 载入--存储(在采用存储器寻址的计算机上为move指令) |
控制 | 分支、跳转、过程调用与返回、陷阱 |
系统 | 操作系统调用、虚拟存储器管理指令 |
浮点 | 浮点运算:加、乘、除、比较 |
十进制 | 十进制加、十进制乘、二进制到字符的转换 |
字符串 | 字符串移动、字符串比较、字符串搜索 |
图形 | 像素与顶点操作、压缩/解压缩操作 |
控制流指令
当控制中的改变是无条件时,使用跳转jump。当改变是有条件时,使用分支branch。
通常可区分4种不同类型的控制流变化:条件分支、跳转、过程调用、过程返回。
控制流指令在一个载入--存储计算机上的出现频率
将控制流指令分为三类:调用或返回、跳转和条件分支。条件分支占大多数。
控制流指令的寻址方式:
控制流指令中的目标地址在任何情况下都必须指定。在大多数情况下,这个目标是在指令中明确指定的,但过程返回是一个例外,这是因为在编译时无法知道要返回的目标位置。
指定目标的最常见方法是提供一个将被加到程序计数器PC的位移量。这类控制流指令被称为PC相对指令。由于目标位置通常与当前指令的距离较近,而且在指定相对于当前PC的位置时,需要的位数较少,所以PC相对分支或跳转具有优势。
采用PC相对寻址可以使代码的云讯不受装载位置的影响,这一特性被称为位置无关,可以在链接程序时减少一些工作,而且对于在执行期间进行动态链接的程序也有参考价值。
如果在编译时不知道目标位置,为了实现返回和间接跳转,需要一种不同于PC相对寻址的方法,即必须有一种动态指定目标的方法,使目标能够在运行时发生变化,这种动态寻址只需要给出包含目标地址的寄存器名称即可,跳转可能允许使用任意寻址方式来提供目标地址。
寄存器间接跳转的其他用途:1.case或switch语句 2.虚拟函数或虚拟方法 3.高阶函数或函数指针 4.动态共享库 在以上的四种情况下,目标地址在编译时都是未知的,通常是在寄存器间接跳转之前从存储器加载到寄存器中。
由于分支通常使用PC相对寻址来指定其目标,一个重要的问题是关注分支目标距离分支有多远
指令中PC相对分支的位移量分布如下:
分支距离(以目标与分支指令之间的指令数来表示)。整数程序中最常见的分支是转向可以用4~8位编码的目标地址。也就是说,短位移量字段对于分支指令是足够的,有了较小分支位移量的较短指令,设计者可以提高编码密度。
对于同一程序,如果体系结构需要的指令较少,那分支距离就较短。但如果计算机的指令长度是变化的,可以与任意字节连接对齐,则表示该位移量所需要的位数可能会增加。
条件分支选项
由于大部分控制流改变都是分支,如何指定分支条件很重要。
对分支条件进行求值的主要方法:
名称 | 示例 | 如何测试条件 | 优点 | 缺点 |
条件码(CC) | 8086、ARM、PowerPC、SPARC、SuperH | 测试由ALU运算设定的特殊位,可能受程序的控制 | 有时条件设置比较自由 | CC是一种额外状态。由于条件码是将来自一条指令的信息传给一个分支,所以限制了指令的顺序 |
条件寄存器 | Alpha、MIPS | 用比较结果测试任意寄存器 | 简单 | 占用一个寄存器 |
比较与分支 | PA_RISC、VAX | 比较是分支的一部分。比较范围通常限于子集 | 分支需要一条指令,而不是两条 | 对流水线执行来说,每个指令要完成的工作过多 |
条件分支中不同比较类型的使用频率。编译器与体系结构的这种组合中,小于分支占主导地位。
过程调用选项
过程调用和返回包括控制转移,还可能涉及一些状态保存过程;至少必须将返回地址保存在某个地方,有时保存在特殊的链接寄存器中,有时只是保存在GPR中。
较早的体系结构提供了一种用于保存许多寄存器的机制,而较新的体系结构需要编译器为所存储和恢复的每个寄存器生成存储和载入操作。
在保存寄存器时,要么保存在调用位置,要么保存在被调用的过程内部。调用者保存是指发出调用的过程必须保存它希望在调用之后进行访问的寄存器。被调用者保存必须保存它希望使用的寄存器,而调用者不受限制。
因为不同的过程编译是独立的,所以两个不同过程中对全局可见变量的访问存在复杂的关系。编译器对这样的过程会采用调用者保存。即由调用者将所有可能在调用期间访问的变量都保存起来。现在大多数实际系统都采用两种机制的组合方式,这一约定在应用程序二进制接口ABI中指定,它确定了一些基本规则,指出哪些寄存器应由调用者保存,哪些由被调用者保存。
以上从编译器的层次,完成了对指令体系结构的设计。
即采用位移量、立即数和寄存器间接寻址方式的载入--存储体系结构。介绍的数据为8位、16位、32位和64位整数,还有32位和64位的浮点数。
指令包括简单操作、PC相对条件分支、用于过程调用的跳转和链接指令,还有用于过程返回的寄存器间接跳转。
指令集编码
在对指令进行编码时,由于寄存器字段和寻址方式字段可能在一条指令中出现许多次,所以寄存器数目和寻址方式的数目都对指令大小有显著影响。对大多数指令而言,对寻址方式字段和寄存器字段进行编码时所占的位数,要远多于指定操作码所占的位数。
常见的指令集编码选择:
1.变长编码:它几乎允许所有操作使用所有寻址方式。当存在许多寻址方式和操作时,是最佳选择。8086,VAX。这种方式的代码表示长度通常是最短的。
结构:操作与操作数数目+地址标识符1+地址字段1+......+地址标识符n+地址字段n
2.定长编码:它将操作和寻址方式合并到操作码中,当寻址方式与操作数较少时,其效果最好。 Alpha、ARM、MIPS。定长格式中的操作数个数是相同的,寻址方式作为操作码的一部分进行指定,生成的代码规模通常是最大的。
结构:操作+地址字段1+地址字段2+地址字段3
3.混合编码
编译器
几乎所有的台式机和服务器应用程序都是用高级语言编写的。意味着:由于所执行的大多数指令都是编译器的输出,所以指令集体系结构基本上就是编译器目标。
编译器的构成:
结构 | 相关性 | 功能 |
语言分析 | 与语言有关、与机器无关 | 将语言转换为公共中间形式 |
高级优化 | 与语言有关、与机器无关 | 循环转换和过程内联化 |
全局优化 | 与语言相关较低,与机器稍微有关 | 全局和本地优化器+寄存器分配 |
代码生成器 | 高度机器相关,语言无关 | 详尽的指令选择和机器相关优化,可能包含汇编器 |
编译器的目标是:编译结果的正确性,编译后代码速度,快速编译、支持调试、语言之间的互操作性。正常情况下,编译器中的各次扫描将更抽象的高级表示转换为逐渐低层级的表示方法,最后到达指令集级别。
编译器假定最后的几个步骤有能力处理特殊的问题。例如,在知道被调用过程的确切大小之前,编译器通常就必须选择对哪些进程调用进行内联展开。
全局公共子表达式消去法:找出一个表达式计算相同取值的两个实例,并将第一次计算的结果值保存在临时存储位置。然后利用这个临时值,清除这一公共表达式的第二次计算。必须将临时值分配到寄存器中。如果没有将临时值保存到寄存器中,这一优化会减缓代码的运行速度。寄存器分配通常是全局优化扫描即将结束、马上要生成代码时进行的。因此,执行这一优化的优化程序必须假定寄存器分配器会将这一临时值分配到寄存器中。
现代编译器执行的优化分类:
高级优化--一般对源代码执行,并将输出结果传送给之后的优化扫描。
本地优化--仅对直行代码段内的代码进行优化
全局优化--将本地优化拓展到分支范围之外,并引入一组专为优化循环的转换
寄存器分配--将寄存器与操作数联系起来
与处理器相关的优化--尝试充分利用特定的体系结构知识
寄存器分配
寄存器分配是最重要的优化,寄存器分配算法以图形着色的技术为基础。图形着色技术的基本思想:构造一幅图,用来表示可能执行的寄存器分配方案,然后利用这个图来分配寄存器。
问题在于如何使用有限种颜色,使相关图中两个相邻结点的颜色都不相同。这种方法的重点是将活跃变量全部分配到寄存器中。当至少有16个通用寄存器可用于为整数变量进行全局分配,而且有其他寄存器分配为浮点变量时,图形着色方法效果最好。
优化名称 | 解释 | 优化转换总数的百分比 |
高级优化:
过程整合
|
在源代码级别或接近该级别,与处理器无关
用过程主体代替过程调用
|
|
本地优化:
公共子表达式消除法
常量传播
降低栈高度
|
在直行代码范围内
用单一副本代替同一计算的两个示例
对于一个被赋值为常量的变量,用该常量代替其所有实例
重新排列表达式树,用以最大限度地减少表达式求值所需的资源
|
18%
22%
|
全局优化:
全局公共制表达式消除法
副本传播
代码移动
消去归纳变量
|
跨越分支
与本地优化相同,但跨越了分支范围
对于一个已经被赋值为X的变量A(A=X),用X代替变量A的所有实例
如果在循环的每次迭代中,其中一些代码总是计算相同值
简化/消去循环内的数组寻址计算
|
13%
11%
16%
2%
|
与处理器相关优化:
降低强度
流水线调度
分支偏移优化
|
依赖于处理器知识
许多示例,比如用加法和移位来代替与常量的乘法
重新排列指令顺序,以提高流水线性能
选择能够到达目标的最短分支位移
|
高级语言用来保存数据的三个独立区域:
1.栈用于分配本地变量。栈会在进程调用与返回时相应增大或缩小。栈内的对象是相对于栈指针进行寻址的,这些对象主要是标量,而不是数组。
栈用于活动记录,而不是用于表达式求值。因此,几乎不会在栈中压入或弹出数值。
2.全局数据区用于静态分配所声明的对象,比如全局变量和常量。这些对象中有很大一部分都是数组或其他聚合数据结构
3.堆用于分配不符合栈规则的动态对象。栈中的对象用指针访问,通常不是标量。
对于分配到栈中的对象,寄存器分配的处理效率要远高于对全局变量的处理效率,而寄存器分配对于分配到堆中的对象基本上不可能实现,因为他们都用指针访问的。
全局变量和一些栈变量可不可能分配,因为他们具有别名,即有多种方法可以引用变量的地址,从而不能合法地将其放到寄存器中。