分享

解读 Intel? Microarchitecture

 盛夏流年闪耀 2014-05-13

以 Intel? sandy bridge 微架构为例,了解一下 Intel 近代微架构。

    上图是 Intel? sandy bridge 微架构的流水线示意图,实行了“发射-执行-完成”相分离包括下面的组件(乱序执行按序完成):

  1. 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。
  2. 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。
  3. in-order retirement,按序完成单元。使用 reorder buffer 保存 uops 各个阶段的结果,确保 uops 的执行结果(包括任何可能遇到的异常,中断)按原始程序的次序完成。
  4. execution unit,执行单元,含有 6 个执行 port 以及 3 个类型的 stack。因此,schedular 每个 cycle 最多可以调度并分派 6 个 uops 到执行 port。
  5. 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 微架构上,ICacheITLB 的明细信息:

  • 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:

  1. store 操作:包括了 sotre 寄存器与立即数。例如:“mov [rdi], rax”指令与“mov DWORD [rdi], 1
  2. load-and-operation 操作:指令的操作数是 register 与 memory,执行的是“load + op”操作(被解码为 load uop 与另一个操作 uop)。
    例如:“add rax, [rsi]”,“addps mmx0,[rsi]”,“xor rax, [rsi]”指令等等...
  3. load-and-jump 操作:这是一条分支指令,目标地址从 memory 地址 load 而来。例如:“jmp QWORD [rax]”,“call QWORD [rax]”指令。RET 指令也是属于 micro-fusion 指令,因为它从 RSP 指向的栈里 load 目标地址(即返回地址)。
  4. 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,允许进行宏融合的两条指令需要满足下面条件:

  1. 第一条指令修改了 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 指令。
  2. 后面的指令是条件分支指令(Jcc 指令)。但是,不同的指令,以及不同的微架构所支持的条件不同。
    • TEST 指令所有的条件(所有的 eflags 标志位),包括:OF,CF, ZF, SF 以及 PF 标志位。
    • CMP 指令根据不同微架构支持不同的条件。
      • core 微架构仅支持 CFZF 标志位。因此,支持下面的 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(调用返回),也就是 RETRET n 指令。使用 16 个 entries 的 RSB(return stack buffer)结构实现。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多