读书笔记 计算机系统--系统架构与操作系统的高度集成 第二章处理器体系结构
处理器设计所围绕的两个问题:
指令集设计(软件)
机器结构设计(硬件)
第二章为指令集设计,第三章为机器结构设计。
指令集设计所受影响:
程序语言(精度,寻址模式,跳转指令)
应用(科学计算 -> 应用)
操作系统(内存管理,程序的不连续性)
指令集如何设计:
早期,硬件可否实现 -> 如今, 是否实际有用。
*能否有利于高级语言编写高效而紧凑的程序。———–一
要优雅,体系结构的选择————————————二
一:指令集与高级语言
高级语言的功能集:
表达式和赋值语句——————————————-(一)
高级数据抽象————————————————-(二)
条件语句和循环———————————————-(三)
编译函数调用————————————————-(四)
(一)表达式和赋值语句
a = b + c;
add a, b, c;高级语言(上)都可以映射为指令(下),称为双操作数指令(两个操作数产生一个结果),也称为三操作数指令(两个源操作数和一个目的操作数)
所有的算数/逻辑运算来说,操作数都在处理器的寄存器中:
1、算数/逻辑运算用的ALU在处理器中,位置更接近。
2、操作数的可寻址性,如果操作数在内存中,随着内存大小的增加,内存的唯一寻址的地址大小也增加,指令大小也会增加。
寄存器数量必须较小,以限制寻址的位数,并引入寻址模式,寄存器寻址。
将操作数从内存搬到寄存器需要两条指令:
加载:ld r2, b; r2 <- b
存储:st r1, a; a <- r1
将操作数搬到寄存器而不是直接使用内存操作数的好处:重用速度快(d=a*b+a*c+b*c;)
为了防止在从内存搬运的地址操作数的长度不受操作数可寻址性的影响,引入基址加偏移量寻址模式。
ld r2, offset(rb); r2 <- MEM[rb + offset]
约定,一个字32位,半字16位,字节8位
操作数宽度与其精度有关:
精度越低内存中占用空间就越小,在算数/逻辑运算时还有一定的时间优势。
操作数精度最好恰好满足类型需求。
指令集应该包含不同精度操作数:
ld r1, offset(rb); 从地址offset+rb处装一个字到r1中
ldb r1, offset(rb); 从地址offset+rb处装一个字节到r1中
可寻址性:与前面的不同,这里的可寻址性指的是内存中能被寻址的最小精度,因为不可能一位一位寻址。
字节序,一个字中的各个字节排列的顺序:
0x11223344
大端模式,高位在小地址
小端模式,高位在大地址
声明某种数据类型,不要用别的精度访问,因为字节序。在网络有关代码需要在不同机器上使用格式转换来回切换。
程序在内存中占据的空间被称为内存印记。
如果数据结构中有多种不同精度的变量,打包很有意义,保证没有空间浪费。
例:struct {char a;char b[3];}可能为这种布局
浪费了50%的存储空间,有效的编译器会将这种情况打包为
上面除了节省空间外,还能减少处理器和内存之间的访问次数,非常高效。
但打包并非总是正确的途径:
struct {char a; int b;} char一个字节,int四个字节,可能为
lsb为低位,msb为高位
上面这种非常低效,为了读取int,需要访问两次,所以往往要求操作数从可寻址地址开始。这就是对齐限制。
所以编译器会使用
尽管浪费了37.5%空间,但变得更加高效了。
综上说明了高级语言的赋值与表达式转换成指令集后在内存中的位置,大小以及排序方式和打包。
(二)高级数据抽象
除了标量意外,高级语言支持的数据抽象(数组,结构)因为庞大的体积,处理器中的寄存器数量不足以支持,只能分配在内存里。
高级语言的结构数据类型,可以通过基址加偏移量的寻址模式执行。
许多编程语言允许数组动态决定大小,因为不知道数组所需要的存储空间,所以内存中可能并非线性存储。
(三)条件语句和循环
if-then-else语句
引入例子beq r1, r2, offset:
1)比较r1和r2
2)如果相等,下一条指令地址为PC+offsetadjusted。(PC为程序计数器当前地址,程序计数器相对寻址)
3)如果不等,下一条指令为跟在beq后面的指令。
条件语句受偏移量大小的限制,8位的偏移量大小只能在(PC-128,PC+127)范围,所以需要引入无条件跳转:
J rtraget。
switch语句
如果case的数量有限,最好就是编译为多个if-then-else语句结构。
如果有许多连续的,不稀疏的case,if-then-else就低效。
另一种选择就是用跳转表记录所有case代码段的起始地址,靠无条件跳转实现。
循环语句
高级语言的循环例子:
j = 0;
loop: b = b + a[j];
j = j + 1;
if (j != 100) go to loop;
假设寄存器r1用于保存变量j,而寄存器r2包含值100,则转换为
loop: …
…
…
beq r1, r2, loop
不需要新指令和寻址模式。
其他的循环(for,while)都能用上述的条件或者非条件分支编译。
(四)编译函数调用
函数调用的过程:
在main函数中调用了函数foo。控制流转移到该函数的入口处。
退出foo时,控制流返回main函数中紧接着goo函数的语句。
引入术语:
调用者caller,做出过程调用的实体。
被调用者callee,被调用的实体。
上述例子中调用者是main函数,被调用者是foo函数。
当调用者调用被调用者时,调用者的寄存器的内容是需要快速保存和恢复,解决方案有硬件和软件两种。
硬件:
使用影子寄存器,用来保存
只有最左边的寄存器是可见的。
特点:快,但会受到嵌套层次的限制。
软件:
将调用点状态保存入栈中,因为栈具有后进先出的特性,满足函数调用需求。
编译器会选择处理器中一个寄存器作为专用的栈指针,非必需,但方便。
特点:内存和处理器之间搬运数据慢,不受嵌套层次限制。
缩小性能差距方法(不保存全部寄存器状态):
调用者获得全部寄存器的一个子集(s寄存器组)随意使用。
被调用者如果要用s寄存器组则需要保存/恢复他们,不用则不考虑。
还有一部分寄存器的子集(t寄存器组),调用者和被调用者可以共用且不需要保存/恢复。
大致流程:
1)参数传递,参数个数超出寄存器限制,用栈来传递多余的参数。
2)记录返回地址,引入 JAL rtarget , rlink :
返回地址保存在rlink 寄存器中,将PC置为rtarget
3)将控制权交给被调用者。
4)被调用者局部变量的空间。
5)返回值,返回值超出寄存器保存范围,用栈来传递。
6)返回道调用点J rlink
软件惯例:
活动记录:
栈中与当前执行的过程有关的一部分区域。
递归:
无需在指令集体系结构上增加任何东西就能支持递归,栈机制保证了每一个实例。
帧指针:
尽管可以跟踪栈指针的移动,但是维护和时间代价大。
帧指针包含当前函数的活动记录的第一个地址,且在过程执行时不会改变。
如果调用其他过程,被调用者需要将帧指针保存到栈,并将当前栈指针复制给帧指针。
二:指令集体系结构的选择
做出这些选择处于当前技术和硬件可行性考虑,有时时为了对高级语言结构提供精简而高效的支持。
额外的指令,提升编译出的代码的空间和时间的有效性。
额外的寻址模式:如间接寻址 ld @(ra),一般走最少化路线。
历史上体系结构类型:
面向栈的体系结构,所有操作数在栈上
面向内存的体系结构,大部分指令都是操作内存中的操作数
面向寄存器的体系结构,本章所讨论的,大部分指令都是操作寄存器中的操作数
混合类型,针对特定应用可以选择其中某一种,如IBM PowerPC和Intel x86
指令格式:
按指令结构分:
零操作数 HALT,NOP
单操作数 INC/DEC NOT
双操作数 ADD, STORE,LOAD,MOV
三操作数ADD,LOAD
广义分:
所有指令等长
优势:简化实现,可以马上对字段进行解释
劣势:可能浪费空间,需要连接逻辑,设计受限
指令长度可变
优势:没有空间浪费,设计不受限,对编译器更有针对性
劣势:复杂,解释困难
影响处理器设计的问题
一个指令集的成败很大以来与市场的接纳程度。
应用程序对指令集设计的影响巨大,视频,音频,游戏,图像等。
其他:操作系统,对现代语言的支持,存储系统,并行性,调试,虚拟化,容错性,安全性等等。