第4章涉及到一些数字电路、硬件控制语言HCL的知识,这里先行跳过,有机会回头补充
4.1 Y86-64指令集体系结构
由于x86-64指令集过于复杂,书中定义了一个简化的Y86-64指令集,阐述处理器的体系结构
4.1.1 程序员可见状态
- 15个64位寄存器:
%rax、%rbx、...、%r14(注:这里不包含%r15是区分操作数有无寄存器的情况)- 其中:
%rsp在入栈、出栈、函数调用和返回操作下,认定为是栈顶指针
- 其中:
- 3个简化的条件码:
ZF、SF、OF(零、符号、溢出),保存最近的算术或逻辑指令造成的影响 - 程序计数器PC:存放当前正在执行的指令的地址
- 内存:提供了一个单一的字节数组映像
- 状态码Stat:表明程序执行的总体状态(程序正常运行或出现某种异常)
4.1.2 Y86-64指令
- 传送指令:
irmovq、rrmovq、mrmovq、rmmovq。源可以是立即数(i)、寄存器(r)或内存(m);目的可以是寄存器(r)或内存(m)。注意不支持内存直接传送到内存、不支持立即数直接传送内存 - 整数操作指令:
addq、subq、andq、xorq。这些指令会设置条件码ZF、SF、OF - 跳转指令:
jmp、jle、jl、je、jne、jge、jg - 条件传送指令:
cmovle、cmovl、cmove、cmovne、cmovge、cmovg - 函数相关:
call、ret - 入栈出栈:
pushq、popq - 停止指令:
halt,使整个系统暂停运行,并将状态码置为HLT
4.1.3 指令编码
- Y86-64指令编码规则:指令长度1字节~10字节,分为3个部分:
- 指令指示符:第一个字节。高4位:代码部分(icode);低4位:功能部分(ifun)
- 寄存器操作:第二个字节。高4位:源寄存器的编码;低4位:目的寄存器的编码。寄存器的编码见图4-4。其中,F表示目的/源操作数不使用寄存器
- 数值操作:8字节常数字,用十六进制表示
- 其中对于
OPq、jXX、cmovXX的指令,用fn区分功能:
- 涉及到对寄存器的操作,将15个寄存器进行编码,F表示无寄存器
【例】确定rmmovq %rsp, 0x123456789abcd(%rdx)的Y86-64指令编码
- 操作rmovq:40
- 源寄存器%rsp:4
- 目的寄存器%rdx:2
- 数字D:按小端序排列为cd ab 89 67 45 23 01 00
40 42 cd ab 89 67 45 23 01 00
4.1.4 Y86-64状态码Stat
| 值 | 名字 | 含义 |
|---|---|---|
| 1 | AOK | 正常操作 |
| 2 | HLT | 执行halt指令 |
| 3 | ADR | 遇到非法地址 |
| 4 | INS | 遇到非法指令 |
4.1.5 Y86-64程序
【例】书中例子,计算一个整数数组的和。与X86-64相比有些许不同:
- 算数指令addq的源寄存器无法直接使用立即数和内存位置,所以有irmovq、mrmovq操作
- subq操作直接设置了条件码ZF,所以省略了testq
4.3 Y86-64的顺序实现
在Y86-64处理器处理一条指令,分成6个步骤:
- 取指(fetch):从内存读取指令字节(1~10个字节),顺序方式计算下一条指令valP。指令字节包括:
- 指令字节:前四位指令(icode)和后四位功能(ifun)
- 寄存器字节:源寄存器(rA)和目的寄存器(rB)
- 常数字:八字节valC
- 译码(decode):从寄存器读取最多两个操作数,得到值(valA/valB)。有些指令还会读取寄存器%rsp
- 执行(execute):ALU执行指令操作,得到结果valE。并设置条件码CC。
- 访存(memory):数据写入内存,或从内存读取值(valM)
- 写回(write back):将执行结果valE写回寄存器
- 更新PC(PC update):PC的值更新为指令下一条的地址
因此,我们可以把4.1.2节的Y86-64指令逐一分析:
- 逻辑运算、传送指令:
- 入栈和出栈操作
- 控制转移指令:
nop:PC+1,不做任何处理halt:处理器状态置位HLT,使处理器停止运行
4.4 流水线的通用原理
- 顺序处理:一条指令处理完,再处理下一条指令。缺点是指令的执行非常慢,一个时钟周期只能处理一条指令
- 流水线处理:指令分若干个独立阶段,处理器可以同时处理各个阶段。在每个阶段间插入一个流水线寄存器:控制一个指令离开系统、另一个指令进入系统。
【注】流水线的局限性:
- 由于每个阶段处理的时间可能不同,所以每一步同样有时间的浪费 (木桶效应)
- 并不是阶段划分越多系统吞吐量越高,过深的流水线反而会带来系统的损耗,需要一定的权衡。
4.5 Y86-64的流水线实现
4.5.1 流水线的实现
- 根据4.3.1节,一条指令分为6个阶段,每个阶段之间插入一个D触发器。根据D触发器的特性,当时钟信号达到上升沿时,输入信号才加载进触发器中并输出,这样就可以控制流水线的时序:
【例】顺序执行案例,5条指令通过流水线。在时刻5,流水线中并行运行5个指令的阶段
4.5.5 流水线冒险
- 引入流水线技术后,有可能会带来执行错误,称为冒险(hazard)。分两种情况:
- 数据冒险:下一条指令会用到上一条指令的结果
- 控制冒险:一条指令确认下一条指令的位置(
jXX、ret)
4.5.5.1 数据冒险
【问题】下一条指令会用到上一条指令的结果,所以流水线执行指令会导致计算错误。就比如:
0x000: irmovq $10, %rdx
0x00a: irmovq $3, %rax
0x014: addq %rdx, %rax
【说明】在执行addq操作时,译码(decode)阶段需要读取%rax的值。而irmovq操作在写回(write back)阶段才更新%rax的值。因此,两者之间要增加处理,保证运算正确执行。书中介绍了几个方案:
- 暂停技术:
addq执行译码前,等待irmovq的写回操作完成。指令间增加nop。缺点是效率较低 - 数据转发:直接使用
irmovq的valE作为addq的valA,而不写回寄存器。好处是节省时钟周期;缺点是要修改硬件结构 - 结合使用:暂停技术+数据转发
【例】书中例子,暂停技术和数据转发
- 暂停技术:增加3个bubble(即nop操作),保证
addq的Decode要在irmovq的Write back之后
- 数据转发:
irmovq的Execute后,输出值valE直接作为addq的输入,而不经过写入+译码寄存器。此时addq的Decode可以和irmovq的Execute处于同一个时钟周期
- 结合使用:
mrmovq的Memory结束后,addq指令才能读取到valA的值。单纯使用数据转发无法解决这种情况,需要结合暂停技术。增加1个bubble
4.5.5.2 控制冒险
控制冒险:处理器无法根据取值阶段的指令确认下一条地址时,出现控制冒险
【问题1】操作是ret时,下一条地址从栈取出,即必须要等到上一条指令的访存(Memory)操作结束
【例】书中ret例子,执行proc函数后返回
0x000: irmovq stack, %rsp
0x00a: call proc # Procedure call
0x013: irmovq $10, %rdx # Return point
0x01d: halt
0x020: .pos 0x20
0x020: proc:
0x020: ret # Return immediately
0x021: rrmovq %rdx, %rbx # Not executed
0x030: .pos 0x30
0x030: stack:
- 为简化说明,转为顺序执行语句:
0x000: irmovq Stack, %edx
0x00a: call proc
0x020: ret
0x013: irmovq $10, %rdx # Return point
【说明】在ret的访存(Memory)阶段才能从栈中获取0x013的PC地址。所以irmovq指令的取值(Fetch)要在ret的访存(Memory)之后。使用暂停技术,增加3个bubble:
【问题2】操作是jXX时,因为指令是并行执行的,下一条指令不知道是执行谁。如果流水线执行,可能导致分支预测错误。真正确定需要等上一条执行(Execute)操作结束
【例】书中jne例子
0x000: xorq %rax, %rax
0x002: jne target
0x00b: irmovq $1, %rax
0x015: halt
0x016: target:
0x016: irmovq $2, %rdx
0x020: irmovq $3, %rbx
0x02a: halt
- 这里采用最简单的分支策略,总是认为要跳转执行 (实际要复杂得多)。此时转为下面的顺序执行指令:
0x000: xorq %rax, %rax
0x002: jne target
0x016: irmovq $2, %rdx
0x020: irmovq $3, %rbx
0x00b: irmovq $1, %rax
0x015: halt
【说明】jXX指令在加入流水线时,流水线会预测选择分支。相当于后续的指令执行了一半,然后jne指令的Execute阶段才判断出是否需要跳转(即是否预测正确,注意执行是乱序的)。因此分预测正确和错误两种情况:
jne预测正确:直接执行0x016和0x020的指令,不执行0x00b的指令jne预测错误:0x016和0x020的指令需要剔除。下一时钟通过插入bubble,取消掉0x016和0x020的执行。另外,下一时钟还要将0x00b的指令加入流水线,如图所示:
4.5.8 流水线控制逻辑
为了实现流水线控制,在流水线寄存器中,增加两个控制输入:暂停(Stall)和气泡(Bubble)。因此,分为三种情况:
- 正常:暂停=0 && 气泡=0。时钟上升沿到达时,寄存器加载输入y,并作为输出
- 暂停(指令暂停):暂停=1 && 气泡=0。时钟上升沿到达时,保持流水线寄存器的状态不改变。
- 气泡(取消指令):暂停=0 && 气泡=1。时钟上升沿到达时,将寄存器的状态清0
- 对于数据冒险和控制冒险,流水线寄存器需要加入暂停或气泡处理:
【注】控制条件远没这么简单,还有组合的情况。详见书本318页控制条件的组合。这里暂不列在笔记中
4.5.9 性能分析
- 使用
CPI(Cycles Per Instruction, 每指令周期数)进行衡量。定义一个阶段处理了条指令和个气泡,则:
- 将视为处罚项,表明一条指令平均要插入多少个气泡。这个值可细分三项:
- 加载处罚(lp, load penalty):加载/使用冒险暂停时,插入气泡的平均数
- 预测错误分支处罚(mp, mispredicted branch penalty):由于分支预测错误,取消指令时插入气泡的平均数
- 返回处罚(rp, return penalty):返回指令造成暂停时,插入气泡的平均数
【例】书中例子
附录
在CMU官网,提供了一个Y86-64的仿真工具,可以模拟C代码的汇编(yas)、执行(yis)。安装简介:
wget http://csapp.cs.cmu.edu/3e/sim.tar
tar xvf sim.tar
# 安装bison和词法分析工具flex
apt-get install bison flex -y
# 仿真工具如果要使用图形界面,则需要安装TCL和TK
apt-get install tcl8.5-dev tk8.5-dev tcl8.5 tk8.5 -y
# 修改sim目录下的Makefile文件
GUIMODE=-DHAS_GUI
TKLIBS=-L/usr/lib/ -ltk8.5 -ltcl8.5
TKINC=-I/usr/include/tcl8.5
# 编译安装
make clean && make
- 以4.1.5节例子为例,编译运行程序
asum.ys:可以看到返回值%rax = 0xabcdabcdabcd,执行正确。
cd y86-code
../misc/yas asum.ys
../misc/yis asum.yo
# 输出结果
Stopped in 34 steps at PC = 0x13. Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax: 0x0000000000000000 0x0000abcdabcdabcd
%rsp: 0x0000000000000000 0x0000000000000200
%rdi: 0x0000000000000000 0x0000000000000038
%r8: 0x0000000000000000 0x0000000000000008
%r9: 0x0000000000000000 0x0000000000000001
%r10: 0x0000000000000000 0x0000a000a000a000
Changes to memory:
0x01f0: 0x0000000000000000 0x0000000000000055
0x01f8: 0x0000000000000000 0x0000000000000013
- 也可以使用仿真器进行仿真调试。有两种仿真形式:顺序执行(SEQ)、流水线执行(PIPE)
# 顺序执行
cd sim/seq
./ssim -g ../y86-code/asum.yo &
# 流水线执行
cd sim/pipe
./psim -g ../y86-code/asum.yo &
- CMU CSAPP学生主页:csapp.cs.cmu.edu/3e/students…
- Y86-64仿真工具:csapp.cs.cmu.edu/3e/sim.tar
- Y86-64操作手册:csapp.cs.cmu.edu/3e/simguide…