以 Intel? sandy bridge 微架构为例,了解一下 Intel 近代微架构。
上图是 Intel? sandy bridge 微架构的流水线示意图,实行了“发射-执行-完成”相分离包括下面的组件(乱序执行按序完成):
- in-order front-end:程序执行顺序的前端组件,包括了:
- L1 ICache 与 ITLB ,指令通过 ITLB 查找 fetch(提取)进入到 32K 的 L1 指令 cache。
- pre-decoder,一个预解码器,主要用来解析指令的长度,处理 LCPs(length changing prefixes)。
- instruction queue,经过初步解析后存放的指令队列。
- decoder,4个解码器。其中一个为复杂解码器,能解码所有 x86/x64 指令。三个简单解码器,负责解码为一个 micro-op。
- decoded ICache,解码后的 uops(micro-ops)cache。
- MSROM(microcode sequencer ROM),一个 microcode sequencer ROM(微代码装置 ROM),存放复杂指令的 micro-op 流。
- BPU(branch prediction unit),分支预测单元。
- micro-op queue,排列 decoded ICache 的 uops。
- out-of-order engine,乱序发射的引擎组件,包括了:
- allocater/renamer,资源分配器与重命名器。Renamer 将 x86 架构性(architectural)的源/目标操作数(寄存器)重命令为微架构性(microarchitectural)源/目标操作数(寄存器),解决 uops 间的 false-dependencies(假依赖),并形成 out-of-order 的“data flow”(数据流)发送到 scheduler。
Allocater 分配 uops 需要的 load buffer 以及 store buffer。
- scheduler,调度器。等待资源可用(dispatch port 可用,read buffer 或者 store buffer 可用)以及 uop 的操作数已经准备好后,将 uop 绑定到相应 dispatch port 后分派到执行单元。scheduler 每个 cycle 最多可以分派 6 个 uops 到执行 port。
- in-order retirement,按序完成单元。使用 reorder buffer 保存 uops 各个阶段的结果,确保 uops 的执行结果(包括任何可能遇到的异常,中断)按原始程序的次序完成。
- execution unit,执行单元,含有 6 个执行 port 以及 3 个类型的 stack。因此,schedular 每个 cycle 最多可以调度并分派 6 个 uops 到执行 port。
- cache hierarchy,包括下面:
- L1 DCache:
- DCU(data cache unit)
- load buffers
- store buffers
- line fill buffers
- L1 ICache
- L2 cache
- LLC(last level cahce)
1. in-order front-end
在按序前端里,包括了下面的组件:
- L1 ICache 与 ITLB ,通过 ITLB 查找 fetch(提取)进入到 32K 的 L1 ICache。
- legacy decode pipeline,下面的组件被划分到 legacy decode pipeline 里:
- pre-decoder,一个预解码器,主要用来解析指令的长度,处理 LCPs(length changing prefixes)。
- instruction queue,经过初步解析后存放的指令队列。
- decoder,4个解码器。其中一个为复杂解码器,能解码所有 x86/x64 指令。三个简单解码器,负责解码为一个 micro-op。
- decoded ICache,解码后的 uops(micro-ops)cache。
- MSROM(microcode sequencer ROM),一个 microcode sequencer ROM(微代码装置 ROM),存放复杂指令的 micro-op 流。
- BPU(branch prediction unit),分支预测单元。
- micro-op queue,排列 decoded ICache 的 uops。
1.1 ICache 与 ITLB
在指令提取(instruction fetch)阶段,处理器通过 ITLB 查找并从内存的 16-byte 边界上提取指令到 ICache 里。当 ICache hit 时引发 ICache 每个 cycle 传送 16 bytes 到指令 pre-decoder 组件里。
如果以平均每条指令 4 个字节来算,那么 ICache 能满足每个周期 4 个 decoder 的解码工作。可以认为,如果遇到较长指令的话(例如 10 个字节),ICache 传送的 16 bytes 是不能满足 decoder 的。
在 sandy bridge 微架构上,ICache 与 ITLB 的明细信息:
- size: 32-Kbyte
- ways: 8
- ITLB 4K-page entries:128
- ITLB large-page(2M/1G)entries:8
也就是说,ICache 共有 32K,8-ways 结构。ITLB 中维护 4K 页面映射结果的表项有 128 个,维护 2M 与 1G 页面映射结果的表项共有 8 个。
当产生 ITLB miss 时,处理器将在 STLB(second TLB,或者 shared TLB)里继续查找。
ITLB miss 而在 STLB hit 时,所需要 7 个 cycles。当然,如果 STLB 也产生 miss 则需要在页表结构里进行 walk,将需要更多的 cycles。
1.2 pre-decoder
pre-decoder 接收从 ICache 发送过来 16 bytes 的指令,它主要执行下面的工作:
- 确定指令的具体长度。
- 处理指令所有的 prefix。
- 标记指令的类型属性。(例如:属于分支指令)
pre-decoder 在每个 cycle 里,最多可以写入 6 条指令到 instruction queue 里。也就是说:在每个 cycle 里,pre-decoder 最多可以从 16-bytes 里解析出 6 条指令放入 instruction queue。要达到每 cycle 6 条指令,这说明平均每条指令不能超过 2 个字节。
如果这 16 字节里包含多于 6 条指令(例如每条指令为 2 个字节),则在下一个 cycle 里 pre-decoder 会继续按每 cycle 最多解码 6 条指令进行解码。
例如,fetch line(16 字节)里含有 7 条指令,那么首 6 条指令会在一个 cycle 完成解码写入 instruciton queue 里,第 7 条指令将在下一个 cycle 里解码。在下一个 cycle 里,ICache 会继续发送 16 bytes 到 pre-pecoder 里,pre-pecoder 继续最多解码 6 条指令。
指令的 operand size 以及 address size 会影响着指令长度。我们知道 CS.D 决定了指令的 default operation size(默认的操作宽度)。当 CS.D = 1 时,默认的 operand size 与 address size 为 32 位。CS.D = 0 时,默认的 operand size 与 address size 为 16 位。
但是,default operand-size override prefix 与 default address-size override prefix 可以改变操作数与地址的宽度,从而改变了指令固定的长度,这两个 prefix 被称为
LCP(length changing prefix)。
- default operand-size override prefix(66H):重新改写默认的 32 位或者 16 位 operand size 为 16 位或者 32 位。
例如:mov eax, 11223344h。它的指令编码为 B8 44 33 22 11(指令长度为 5),当插入 66H 字节时,指令编码为 66 B8 44 33(指令长度为 4)。
- default address-size override prefix(67H):重新改写默认的 32 位或者 16 位 address size 为 16 位或者 32 位。
例如:mov eax, [11223344h],它的指令编码为 A1 44 33 22 11(指令长度为 5),当插入 67H 字节时,指令编码为 67 A1 44 33(指令长度为 4)
当然,也可能存在超过 1 个 LCP 的情况(同时存在 66H 与 67H)。因此,如果指令含有 LCP 的话,pre-decoder 需要确定最终的指令长度,在解码 LCP 时需要额外花费 3 个 cycles (注:在前一代架构中,解码 LCP 需要花费 6 个 cycles)。
另外,REX prefix 虽然也能改变指令的长度(MOV reg, [disp32] 或者 MOV reg64, imme64),但 pre-decoder 解码时并不会花费额外的 cycles。
1.3 instruction queue
instruction queue 组件在 pre-decoder 与 decoder 之间,经过 pre-decoder 后指令的长度已经确认,从 ICache 传送过来的 16-bytes 被解析为 x86 指令写入 instruction queue(指令队列)里存放着,instruction queue 最多可以容纳 18 条指令。由于 macro-fused(宏融合,两条指令解码为 1 个 uop)的存在,instruction queue 每个 cycle 最多传送 5 条指令(其中包括了两条可以宏融合的指令)到 decoder 进行解码。
1.4 decoder
decoder 负责将 x86/x64 的 CISC 指令解码为单一功能的 micro-ops(uops,微操作),从 core 微架构开始就拥有 4 个 decoder(解码器)。第 1 个 decoder(decoder 0) 是复杂解码器,能解码所有的 x86/x64 指令,
每个 cycle 最多可以解码为 4 个 uops。其余 3 个为简单解码器,将简单指令解码为 1 个 uop,每个 cycle 只能解码 1 个 uop。
第 1 个复杂解码器也能解码简单指令,因此 4 个 decoder 都能解码为 1 个 uop,包括 micro-fused(微融合),stack pointer tracking(栈指针跟踪)以及 macro-fused(宏融合)。但是,只有一个 decoder 能解码为 4 个 uops。那么,4 个 decoder 每个 cycles 最多可以解码为 7 个 uops(4 + 1 + 1 + 1)。decoder 解码后的 uops 被送入到 decoded ICache 以及 micro-op queue。
MSROM 组件负责提供复杂的 uops 数据流,在 sandy bridge 微架构里 MSROM 每个 cycle 能提供 4 个 uops,MSROM 用来帮助 decoder 解码超过 4 个 uops 的指令。因此,当指令超过 4 个 uops 将从 MSROM 里取得。向 MSROM 取 uops 的操作可以由 decoder 或者 decoded ICache 组件发起。
通过 macro-fusion(宏融合)技术,decoder 能将两条 x86 指令解码为 1 个 uop。4 个 decoder 都可以产生 macro-fused 动作,但在每个 cycle 里 4 个 decoder 只能产生一个 macro-fused。因此,在每个 cycle 里 decoder 最多能解码 5 条 x86 指令(2 + 1 + 1 + 1),最多能产生 7 个 uops。
1.4.1 micro-fusion(微融合)
x86/x64 指令允许使用“memory-to-register”类操作数,当指令的操作数是 memory 与 register 时将解码为多个 uops。例如:“ADD RAX, [RBX]”指令是一条典型的操作数为 memory 与 register 的指令,它会被解码为两个 uops:一个为 load uop,另一个为 add uop。
考查下面的一条 store 操作指令:
mov [rbx + rcx * 8 + 0Ch], rax
--------------------- ---
| |
| +--------> store data 操作(使用 port 4)
|
+------------------------> store address 操作(使用 port 2 或 port 3)
decoder 0 生成两个 uops:一个是 store address 操作 uop,可以通过 port 2 或者 3 执行。一个是 store data 操作 uop,通过 port 4 执行。但在 dispatch 到 execution unit(执行单元)时这两个单一的 uop 被融合为一个复杂的 uop。
micro-fused(微融合)允许将多个 uops 融合为 1 个复杂的 uop 进行 dispatch 到 execution unit(执行单元)。在发射到执行单元时,这个复杂的 uop 与单一的 uop 花费同样的 cycles。因此,使用 micro-fused 将提高从 decoder 发射到 execution unit 的吞吐量。但是,在执行单元中仍然是执行两个 uops 操作。
从 core 微架构开始引入了 micro-fusion 功能,可以将下面几类操作进行 micro-fused:
- store 操作:包括了 sotre 寄存器与立即数。例如:“mov [rdi], rax”指令与“mov DWORD [rdi], 1”
- load-and-operation 操作:指令的操作数是 register 与 memory,执行的是“load + op”操作(被解码为 load uop 与另一个操作 uop)。
例如:“add rax, [rsi]”,“addps mmx0,[rsi]”,“xor rax, [rsi]”指令等等...
- load-and-jump 操作:这是一条分支指令,目标地址从 memory 地址 load 而来。例如:“jmp QWORD [rax]”,“call QWORD [rax]”指令。RET 指令也是属于 micro-fusion 指令,因为它从 RSP 指向的栈里 load 目标地址(即返回地址)。
- memory 与 immediate 之间的 cmp-or-test 操作:CMP 或者 TEST 指令的操作数是 memory 与 immediate。例如:“cmp DWORD [rsi], 1”,“test DWORD [rsi], 1”指令等。
在 64-bit 模式下,指令使用 RIP-relative 寻址的 memory 时,在下面的情形下不能产生 micro-fused:
- 指令的另一个操作数是 immediate。例如:“mov DWORD [rip + 50h], 400”,“cmp QWORD [rip + 50h], 1”指令等。
- RIP-relative 寻址出现在分支指令里。例如:“ jmp QWORD [rip + 50h]”指令。
1.4.2 macro-fusion(宏融合)
macro-fused 将两条 x86 指令融合为一个 uop,允许进行宏融合的两条指令需要满足下面条件:
- 第一条指令修改了 eflags/rflags 寄存器(不同微架构所支持的指令也不同)。
- core, nehalem 微架构上只支持 CMP 与 TEST 指令。
- sandy bridge 微架构上支持 CMP, TEST, ADD, SUB, AND, INC 以及 DEC 指令。
如果存在两个操作数(operand 1 与 operand 2),这些指令能产生 macro-fused 还需要满足的条件是:operand 1(目标操作数)是 register,并且 operand 2(源操作数)是 immediate,register,或者非 RIP-relative 寻址的 memory。或者 operand 1 是 memory,而 operand 2 是 register。
- REG-REG:例如 cmp eax, ecx 指令。
- REG-IMM:例如 cmp eax, 1 指令。
- REG-MEM:例如 cmp eax, [esi] 指令(非 RIP-relative 寻址)。
- MEM-REG:例如 cmp [esi], eax 指令。
但是,MEM-IMM 操作数不能产生 macro-fused,例如 cmp DWORD [eax], 1 指令。
- 后面的指令是条件分支指令(Jcc 指令)。但是,不同的指令,以及不同的微架构所支持的条件不同。
- TEST 指令所有的条件(所有的 eflags 标志位),包括:OF,CF, ZF, SF 以及 PF 标志位。
- CMP 指令根据不同微架构支持不同的条件。
- core 微架构仅支持 CF 与 ZF 标志位。因此,支持下面的 Jcc 指令:
- JC/JB/JNAE:CF = 1
- JNC/JNB/JAE:CF = 0
- JZ/JE:ZF = 1
- JNZ/JNE:ZF = 0
- JBE/JNA:CF = 1 or ZF = 1
- JA/JNBE:CF = 0 and ZF = 0
- nehalem 微架构增加了对 SF <> OF 与 SF == OF 条件的支持:
- JL/JNGE:SF <> OF
- JNL/JGE:SF = OF
- JLE/JNG:SF <> OF or ZF = 1
- JNLE/JG:SF = OF and ZF = 0
- sandy bridge 微架构增加了对 ADD, SUB, AND, INC 以及 DEC 指令的支持,它支持的条件如下表所示。
条件
|
分支指令
|
TEST
|
AND
|
CMP
|
AND
|
SUB
|
INC
|
DEC
|
OF = 1
|
JO
|
Y
|
Y
|
N
|
N
|
N
|
N
|
N
|
OF = 0
|
JNO
|
CF = 1
|
JC/JB/JNAE
|
Y
|
Y
|
Y
|
Y
|
Y
|
N
|
N
|
CF = 0
|
JNC/JNB/JAE
|
ZF = 1
|
JZ/JE
|
Y
|
Y
|
Y
|
Y
|
Y
|
Y
|
Y
|
ZF = 0
|
JNZ/JNE
|
CF = 1 or ZF = 1
|
JBE/JNA
|
Y
|
Y
|
Y
|
Y
|
Y
|
N
|
N
|
CF = 0 and ZF = 0
|
JNBE/JA
|
SF = 1
|
JS
|
Y
|
Y
|
N
|
N
|
N
|
N
|
N
|
SF = 0
|
JNS
|
PF = 1
|
JP/JPE
|
PF = 0
|
JNP/JPO
|
SF <> OF
|
JL/JNGE
|
Y
|
Y
|
Y
|
Y
|
Y
|
Y
|
Y
|
SF = OF
|
JGE/JNL
|
SF <> OF or ZF = 1
|
JLE/JNG
|
SF = OF and ZF = 0
|
JG/JNLE
|
上表中,Y 表示支持宏融合,N 表示不支持宏融合。
在 core 微架构里,不支持 signed 数的比较产生宏融合(即 JL/JNGE, JGE/JNL, JLE/JNG 以及 JG/JNLE)。这个情况在 nehalem 微架构里得到改善,支持 signed 数的比较结果产生宏融合。
1.4.3 stack pointer tracker
PUSH, POP, CALL, LEAVE 以及 RET 指令会隐式地更新 stack pointer 值,在 core 微架构之后 decoder 负责维护这个隐式的更新 stack pointer 操作。
思考一下这条指令 “push rax”,它在以前的微架构中会产生多个 uops,大概处理如下面所示:
(1) TEMP = RAX ;; ===> renaming ?
(2) RSP = RSP - 8 ;; ===> 生成 ALU uop
(3) [RSP] = TEMP ;; ===> 生成 STA(store address)uop 与 STD(store data)uop
那么,根据上面的拆分,decoder 大致可以解码为 3 个 uops:1 个 SUB uop,1 个 STA(store address) uop 以及 1 个 STD(store data) uop。
再来看看这两条指令“pop rax”与“ret”,大概处理如下面所示:
pop rax :
(1) rax = [RSP] ;; ===> 生成 LD uop
(2) RSP = RSP + 8 ;; ===> 生成 ADD uop
ret :
(1) RIP = [RSP] ;; ===> 生成 JMP uop
(2) RSP = RSP + 8 ;; ===> 生成 ADD uop
pop rax 指令可以解码为 2 个 uop:1 个 LD(load data) uop 与 1 个 ADD uop。ret 指令可以解码为 2 个 uop:1 个 JMP uop 与 1 个 ADD uop。
引进 stack pointer tracker (栈指针跟踪器)这个功能后,将隐式栈指针更新操作移到 decoder 里实现,从而释放了 execution unit(执行单元)资源,增加了发射与执行带宽。PUSH 指令需要 2 个 uops,而 POP 与 RET 只需要 1 个 uop。
1.5 decoded ICache
由于 x86 指令的不定长以及指令解码 uops 数量的不同,需要使用 decoded ICache 来缓存从 decoder 里解码出来的 uops。送入 decoder 进行解码的 16 bytes 可能会解析出少于 4 条 x86 指条或者多于 4 条(按平均每条指令 4 个 bytes 来算),解码出来的 uops 数量也会不同,而 out-of-order 执行单元每个 cycle 最多允许执行 6 个 uops。
造成前端的解码与后端执行的 uops 不匹配,引入 decoded ICache 能很大程度地缓解这些不匹配而带来的 bandwidth(带宽)瓶颈。
decoded ICache 是 8-ways 32-sets 结构,如下图所示:
每 set 的每个 way 最多能容纳 6 个 uops。因此,理想状态下整个 decoded ICache 能缓存 6 * 8 * 32 = 1536 个 uops。
每个 way 装载的 uops 是由 x86 指令字节里的 32 bytes 边界解码出来的(x86 指令 32 字节边界对齐),也就是以 32 bytes 为一个块,作为装载单位。
- 如果 32 bytes 指令块解码出来不足 6 个 uops 时,则 way 不会被填满而留下空位。下一个 32 bytes 指令块解码的 uop 会装入下一个 way 里。
- 如果 32 bytes 指令块解码出来的 uops 超过 6 个时,表明一个 way 不能装下全部 uops,则余下的 uops 会装载到下一个 way 里。最多有 3 个连续的 ways 来容纳这些 uops。也就是 32 bytes 的指令块解码出来的 uops 最多只能装入 3 个 ways 里。那么,允许一个 32 bytes 指令块最多只有 18(6 * 3) 个 uops 可以装入 decoded ICache 中。
除了上面,way 的装填还有一些限制:
- 如果一条 x86 指令解码为多个 uops 时,这些 uops 不能跨 way 装载,只能放入同一个 way 里。
- 每个 way 里最多只能装入 2 个分支 uops
- 当指令从 MSROM 里获得 uops 时,这些 uops 必须独占一个 way。
- 非条件分支 uop 必须是 way 里的最后一个 uop。
- 如果指令含有 64 位的立即数,这个立即数必须占用两个 way。
当由于这些限制而造成 uops 不能装入 decoded ICache 时(例如 32 bytes 指令块解码出来可能超过 18 个 uops),这些 uops 被直接发送到 out-of-order engine。
decoded ICache 是 L1-ICache(instruction cache)和 ITLB 对应的一份 shadow cache,ICache 存放的是 x86 指令字节码,而 decoded ICache 存放的是 uops。也就是说:decoded ICache 里缓存的任何一个 uops 都在 ICache 里存在着对应的 x86 指令。那么,刷新 ICache lines 的指令时,也必须刷新 decoded ICache 里指令对应 uops。
当 ITLB 的某个 entry(或全部)被刷新时,可能造成整个 ICache 被刷新,同时也会使得整个 decoded ICache 被刷新。例如:更新 CR3 寄存器值从而更新了整个页转换表结构(或者 CR4 寄存器某些页机制相关的控制位被更新)。
1.6 BPU (branch prediction unit)
流水线前端利用 BPU(分支预测单元)尽可能地在确定分支指令的执行路径之前就预测出分支的目标地址。
BPU 能预测下面的分支类型:
- conditional branches(条件分支):也就是 Jcc 指令族。
test eax, eax
jz @taken
... ... ;; 分支跳转不成立
@taken:
... ... ;; 分支跳转成立
BPU 预测这个分支跳转的目标地址。也就是预测这个跳转是否成立。
- direct calls/jumps(直接的调用与跳转),它们的目标地址是基于 RIP 与 offset 值而来。如下面代码所示:
jmp @target ;; 直接跳转
... ...
@target:
... ...
call @fun ;; 直接调用
- indirect calls/jumps(间接的调用与跳转),它们的目标地址从 register 或 memory 里读取。如下面代码所示:
@fun: __func
... ...
@target: __target
jmp DWORD [@target] ;; 间接跳转
;;
;; 或者:
;; mov eax, __target
;; jmp eax
... ...
__target:
... ...
call DWORD [@fun] ;; 间接跳转
;;
;; 或者:
;; mov eax, __func
;; call eax
- returns(调用返回),也就是 RET 或 RET n 指令。使用 16 个 entries 的 RSB(return stack buffer)结构实现。
|