【CSAPP笔记】第四章 处理器体系结构

503 阅读11分钟

第4章涉及到一些数字电路、硬件控制语言HCL的知识,这里先行跳过,有机会回头补充

4.1 Y86-64指令集体系结构

由于x86-64指令集过于复杂,书中定义了一个简化的Y86-64指令集,阐述处理器的体系结构

4.1.1 程序员可见状态

  • 15个64位寄存器%rax%rbx、...、%r14(注:这里不包含%r15是区分操作数有无寄存器的情况)
    • 其中:%rsp在入栈、出栈、函数调用和返回操作下,认定为是栈顶指针
  • 3个简化的条件码ZFSFOF(零、符号、溢出),保存最近的算术或逻辑指令造成的影响
  • 程序计数器PC:存放当前正在执行的指令的地址
  • 内存:提供了一个单一的字节数组映像
  • 状态码Stat:表明程序执行的总体状态(程序正常运行或出现某种异常)

image-20221208225619302.png

4.1.2 Y86-64指令

  1. 传送指令irmovqrrmovqmrmovqrmmovq。源可以是立即数(i)、寄存器(r)或内存(m);目的可以是寄存器(r)或内存(m)。注意不支持内存直接传送到内存、不支持立即数直接传送内存
  2. 整数操作指令addqsubqandqxorq。这些指令会设置条件码ZF、SF、OF
  3. 跳转指令jmpjlejljejnejgejg
  4. 条件传送指令cmovlecmovlcmovecmovnecmovgecmovg
  5. 函数相关callret
  6. 入栈出栈pushqpopq
  7. 停止指令halt,使整个系统暂停运行,并将状态码置为HLT

4.1.3 指令编码

  • Y86-64指令编码规则:指令长度1字节~10字节,分为3个部分:
    • 指令指示符:第一个字节。高4位:代码部分(icode);低4位:功能部分(ifun)
    • 寄存器操作:第二个字节。高4位:源寄存器的编码;低4位:目的寄存器的编码。寄存器的编码见图4-4。其中,F表示目的/源操作数不使用寄存器
    • 数值操作:8字节常数字,用十六进制表示

image-20221208231419206.png

  • 其中对于OPqjXXcmovXX的指令,用fn区分功能:

image-20221208231719730.png

  • 涉及到对寄存器的操作,将15个寄存器进行编码,F表示无寄存器

image-20221208231446303.png

【例】确定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

名字含义
1AOK正常操作
2HLT执行halt指令
3ADR遇到非法地址
4INS遇到非法指令

4.1.5 Y86-64程序

【例】书中例子,计算一个整数数组的和。与X86-64相比有些许不同:

  • 算数指令addq的源寄存器无法直接使用立即数和内存位置,所以有irmovq、mrmovq操作
  • subq操作直接设置了条件码ZF,所以省略了testq

image-20221208232727846.png

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指令逐一分析:

  • 逻辑运算、传送指令:

image-20221210175837346.png

image-20221210180831350.png

image-20221210181010887.png

  • 入栈和出栈操作

image-20221210175927449.png

  • 控制转移指令:

image-20221210180004300.png

  • nop:PC+1,不做任何处理
  • halt:处理器状态置位HLT,使处理器停止运行

4.4 流水线的通用原理

  • 顺序处理一条指令处理完,再处理下一条指令。缺点是指令的执行非常慢,一个时钟周期只能处理一条指令
吞吐量=1条指令(20+300)ps1000ps1ns3.12GIPS吞吐量=\frac{1条指令}{(20+300)ps}\cdot\frac{1000ps}{1ns}\approx3.12GIPS

image-20221210182620022.png

  • 流水线处理指令分若干个独立阶段,处理器可以同时处理各个阶段。在每个阶段间插入一个流水线寄存器:控制一个指令离开系统、另一个指令进入系统。
吞吐量=1条指令(20+100)ps1000ps1ns8.33GIPS吞吐量=\frac{1条指令}{(20+100)ps}\cdot\frac{1000ps}{1ns}\approx8.33GIPS

image-20221210182641017.png

【注】流水线的局限性:

  1. 由于每个阶段处理的时间可能不同,所以每一步同样有时间的浪费 (木桶效应)
  2. 并不是阶段划分越多系统吞吐量越高,过深的流水线反而会带来系统的损耗,需要一定的权衡。

image-20221210182934758.png

4.5 Y86-64的流水线实现

4.5.1 流水线的实现

  • 根据4.3.1节,一条指令分为6个阶段,每个阶段之间插入一个D触发器。根据D触发器的特性,当时钟信号达到上升沿时,输入信号才加载进触发器中并输出,这样就可以控制流水线的时序:

image-20221210183910214.png

【例】顺序执行案例,5条指令通过流水线。在时刻5,流水线中并行运行5个指令的阶段

image-20221210184417512.png

4.5.5 流水线冒险

  • 引入流水线技术后,有可能会带来执行错误,称为冒险(hazard)。分两种情况:
    • 数据冒险:下一条指令会用到上一条指令的结果
    • 控制冒险:一条指令确认下一条指令的位置(jXXret

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。缺点是效率较低
  • 数据转发:直接使用irmovqvalE作为addqvalA,而不写回寄存器。好处是节省时钟周期;缺点是要修改硬件结构
  • 结合使用:暂停技术+数据转发

【例】书中例子,暂停技术和数据转发

  • 暂停技术:增加3个bubble(即nop操作),保证addq的Decode要在irmovq的Write back之后

image-20221210220826457.png

  • 数据转发irmovq的Execute后,输出值valE直接作为addq的输入,而不经过写入+译码寄存器。此时addq的Decode可以和irmovq的Execute处于同一个时钟周期

image-20221210215529656.png

  • 结合使用mrmovq的Memory结束后,addq指令才能读取到valA的值。单纯使用数据转发无法解决这种情况,需要结合暂停技术。增加1个bubble

image-20221211100012774.png

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

image-20221211083522230.png

【问题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的指令加入流水线,如图所示:

image-20221211084417584.png

4.5.8 流水线控制逻辑

为了实现流水线控制,在流水线寄存器中,增加两个控制输入:暂停(Stall)和气泡(Bubble)。因此,分为三种情况:

  • 正常:暂停=0 && 气泡=0。时钟上升沿到达时,寄存器加载输入y,并作为输出
  • 暂停(指令暂停):暂停=1 && 气泡=0。时钟上升沿到达时,保持流水线寄存器的状态不改变。
  • 气泡(取消指令):暂停=0 && 气泡=1。时钟上升沿到达时,将寄存器的状态清0

image-20221211091023013.png

  • 对于数据冒险和控制冒险,流水线寄存器需要加入暂停或气泡处理:

1671964608693.jpg

【注】控制条件远没这么简单,还有组合的情况。详见书本318页控制条件的组合。这里暂不列在笔记中

4.5.9 性能分析

  • 使用CPI(Cycles Per Instruction, 每指令周期数)进行衡量。定义一个阶段处理了CiC_i条指令和CbC_b个气泡,则:
CPI=Ci+CbCi=1.0+CbCiCPI=\frac{C_i+C_b}{C_i}=1.0+\frac{C_b}{C_i}
  • Cb/CiC_b/C_i视为处罚项,表明一条指令平均要插入多少个气泡。这个值可细分三项:
    • 加载处罚(lp, load penalty):加载/使用冒险暂停时,插入气泡的平均数
    • 预测错误分支处罚(mp, mispredicted branch penalty):由于分支预测错误,取消指令时插入气泡的平均数
    • 返回处罚(rp, return penalty):返回指令造成暂停时,插入气泡的平均数

【例】书中例子

image-20221211095509802.png

附录

在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 &

image-20221210174522341.png