第3章 程序的机器级表示
3.4 访问信息
操作数指示符
立即数:
用来表示常数;
寄存器:
ra表示任意类型的寄存器,R[ra]表示引用其值;
内存引用:
根据有效地址访问内存
类型 | 格式 | 操作数值 | 名称 |
---|---|---|---|
立即数 | $Imm | Imm | 立即数寻址 |
寄存器 | ra | R[ra] | 寄存器寻址 |
存储器 | Imm | M[Imm] | 绝对寻址 |
存储器 | (ra) | M[R[ra]] | 间接寻址 |
存储器 | Imm(rb) | M[Imm+R[rb]] | 基址+偏移量寻址 |
存储器 | (rb,ri) | M[R[rb]+R[ri]] | 变址寻址 |
存储器 | (rb,ri,s) | M[R[rb]+R[ri]·s] | 比例变址寻址 |
存储器 | Imm(rb,ri,s) | M[Imm+R[rb]+R[ri]·s] | 比例变址寻址 |
数据传送指令
MOV类,把数据从源位置赋值到目标位置。源操作数为存储于寄存器或内存中的立即数,目的操作数为存储器或内存地址。
根据数据长度分为四种:movb传送字节、movw传送字、movl传送双字、movq传送四字。
若xp in %rdi、y in %rsi,则
C代码 | 汇编指令 | 源操作数 | 目的操作数 | 意义 |
---|---|---|---|---|
long x = *xp | movq | (%rdi) | %rax | 指针的间接引用,内存至寄存器 |
*xp = y | movq | %rsi | (%rdi) | 寄存器至内存 |
3.5 算术和逻辑操作
加载有效地址
leaq,将有效地址写入寄存器,目的操作数必须为寄存器。leaq可灵活用于加法及有限形式的乘法。
若x in %rdi、y in %rsi,则
C代码 | 汇编指令 | 源操作数 | 目的操作数 | 意义 |
---|---|---|---|---|
long t = x + 4 * y | leaq | (%rdi, %rsi, 4) | %rax | 加法与乘法组合运算 |
long t = 7 + 9 * x | leaq | 7(%rdi, %rdi, 8) | %rax | 加法与乘法组合运算 |
3.6 控制
条件码
描述算数和逻辑操作的属性,除leaq指令外,所有指令均有条件码。
- CF:进位标志,最高位产生进位,可用来检查无符号数操作溢出
- ZF:零标志,操作结果为零
- SF:符号标志,操作结果为负数
- OF:溢出标志,补码溢出
跳转指令编码
相对地址::
将目标指令地址与紧跟在跳转指令后的下一条指令地址之间的差作为编码,地址偏移量为1、2或4个字节
绝对地址:
用4个字节地址直接指定目标
条件传送实现条件分支
传统方法是通过使用控制条件的转移,即当满足条件时程序沿着一条执行路径执行,不满足时执行另一条路径,性能低效。
使用数据条件转移,即计算一个条件的两种结果,再根据条件是否满足选取其中一个,CPU利用流水线技术可实现指令级并发。
// 低效版本
long absdiff(long x, long y)
{
long result;
if (x < y)
result = y - x;
else
result = x - y;
return result;
}
// 使用数据条件转移的改进版本
long cmovdiff(long x, long y)
{
long rval = y - x;
long eval = x - y;
long ntest = x >= y;
if (ntest)
rval = eval;
return rval;
}
3.7 过程
过程提供一种封装代码的方式,用一组指定的参数和一组可选的返回值实现某种功能。过程P调用Q时执行的动作:
- 传递控制:程序计数器在进入Q时设置为Q的起始地址,返回P时设置为P中调用Q的下一条指令的地址
- 传递数据:P向Q传递参数,Q向P返回参数;
- 分配和释放内存:进入Q时为Q中的局部变量分配空间,返回P时释放这些空间;
3.8 数组分配与访问
基本原则
对于数据类型
和整型常数
,声明
,
为类型
的大小,则在内存分配
个字节。
如int E[i],E的地址存放于寄存器%rdx, i存放与于寄存器%rcx,则指令
会读取内存地址 的值,并将结果赋给寄存器%eax。
指针运算
如果p是一个指向类型为T的数据的指针,p的值为 ,则 。对于上式 :
表达式 | 类型 | 值 | 汇编指令 |
---|---|---|---|
E | int* | movq %rdx, %rax | |
E[i] | int | M[ ] | movl (%rdx, %rcx, 4), %eax |
E[i-3] | int | M[ ] | movl -12(%rdx, %rcx, 4), %eax |
&E[2] | int* | leaq 8(%rdx), %rax | |
E+i-1 | int* | leaq -4(%rdx, %rcx, 4), %rax | |
&E[i] - E | long | i | movq %rcx, %rax |
嵌套数组
声明int A[5][4],A等价于指向指向int[3]的指针。
数组元素在内存中按照“行优先”顺序排列,即A[0]、A[1]分别表示第1、2行一维数组。
声明 ,则数组元素 的内存地址为 。
3.9 异质的数据结构
数据对齐
对齐限制
简化了处理器和内存系统之间接口的硬件设计。
假设CPU总是从内存中取8个字节,则要求地址必须是8的倍数。若保证所有double类型数据的地址对齐到8的倍数,则可以用一个内存操作完成读写,否则需执行两次操作。
对齐原则:任何 字节的基本对象的地址必须是 的倍数。
- stuct S1 {int i; char c; int j;};
4字节对齐,需12字节存储。 - struct S2 {int i; int j; char c;};
若打包成9字节,所有字段均满足对齐要求。但考虑声明struct S2 d[4],若S2类型数据分配9个字节,不可能满足d的每个元素对齐的要求。为满足对齐要求,S2仍会被分配12字节。 - struct P1 {int i; char c; char d; long j;};
8字节对齐,需16字节存储,其中i、c和d占8字节,j占8字节。 - struct P2 {short w[3]; char c[3];};
2字节对齐,需10字节,其中w占6字节,c占4字节。 - struct P3 {short w[5]; char *c[3];};
8字节对齐,需40字节,其中w占16字节,c占24字节。 - struct P4 {struct P2 a[2]; struct P1 t;};
8字节对齐,需24字节,其中a占24字节,t占16字节。
第4章 处理器体系结构
指令集体系结构(Instruction-Set Architecture, ISA),处理器支持的指令和指令的字节级编码。
4.1 Y86-64指令集体系结构
程序员可见状态
Y86-64程序中的每条指令都会读取或修改状态的某些部分,这称为程序员可见状态,"程序员"亦指编译器。
内存- -很大的字节数组,认为虚拟内存系统向Y86-64程序提供一个单一的字节数组映像。
状态码Stat是程序状态的最后一个部分,其表明程序执行的总体状态,如正常或异常。
逻辑门
(1)a和b相等时输出1,否则输出0,表达式bool eq = (a && b) || (!a && !b),图1左图
(2)s为0时输出a,s为1时输出b,表达式bool out = (s && a) || (!s && b)bool out = (s && a) || (!s && b),图1右图
4.3 Y86-64的顺序实现
将处理组织成阶段
取指、译码、执行、访存、写回、更新
取指(fetch)
从内存读取指令字节,地址为程序计数器(PC)的值。指令包含两个四位部分,称为icode(指令代码)和ifun(指令功能)。
译码(decode)
从寄存器文件读入最多两个操作数。
执行(execute)
算术/逻辑单元(ALU)执行指令执行的操作,计算内存引用的有效地址,或改变栈指针。
访存(memory)
将数据写入内存,或从内存读取数据。
写回(write back)
最多写两个结果到寄存器文件。
更新PC(PC update)
将PC设置成下一条指令的地址。
4.4 流水线通用原理
计算流水线
假设将系统执行的计算分成A、B和C三个阶段,每个阶段需要100ps。将各个阶段之间放在流水线寄存器(pipeline register),这样每条指令都会按照三步(三个时钟周期)经过这个系统。稳定状态下,三个阶段都应是活动的。
假设A、B和C组合逻辑需300ps,加载寄存器需20ps,则
若单独执行各阶段,每阶段需100ps,各阶段之间放上流水线寄存器,使每条指令经3阶段处理完成。图2中I1-3为3条不同的指令。
图2中A、B和C三个阶段可同时进行,即I1从A进入B,就可以让I2进入A,因此时钟周期可视为120ps,吞吐量为8.33 GIPS,提升8.33/3.12=2.67倍。
流水线的局限性
-
不一致的划分
若通过各阶段的的延迟从50ps到150ps不等,通过所有阶段的延迟和仍为300ps。运行速率由最慢的阶段限制,故单阶段完成需150+20 = 170ps,吞吐量为5.88 GIPS。 -
流水线过深,收益反而下降
组合逻辑被分为较小块时,寄存器的更新引起的延迟限制了阶段数增加所提高的收益。
如将计算分为6端,每段需50ps,寄存器更新需20ps,则吞吐量为1/(50+20)=14.29 GIPS。可见阶段数加倍,但性能仅提高14.29/8.33=1.71倍。同时,阶段数增加会带来一系列设计难题。