超标量处理器

超标量处理器

  • 概述
    • 超标量处理器的流水线重叠是通过硬件实现的,VLIW是通过程序员和编译器实现的;一个适用通用,一个适用专用,一个硬件复杂,软件简单,一个软件复杂,硬件简单;
    • 时钟和周期自己的理解
      • 同步CPU需要时钟,MOS晶体管是有电阻,两级之间是有电容的,CMOS电路是有延迟的,两个输入端延迟不一样可以加一个触发器去处理;但是这个时延只是被隐藏了,还是存在的,那整个电路的时延都存在,还不一样,数据就没法处理;所以,需要时钟周期来容纳传播时延,并且时钟周期应当大到需要容纳所有逻辑门的传播时延。这个周期结束统一处理,下个周期开始再干别的,周期结束再次统一处理;
      • 周期=所有指令执行周期的最大值
      • 实现周期尽量一致的方法
        • 合并
          • 会使得周期变长,主频变小,则适用对性能要求不高的低功耗嵌入式处理器
        • 拆分
          • 会导致硬件消耗变大,需要更多的寄存器,控制逻辑,相应的端口也要增加,整体的功耗也就增加。
          • 会增加转移预测(分支预测)的失败惩罚特别严重,知道失败的时候已经运行了好几个周期了;
    • 顺序执行和乱序执行
      • Topic1

      • 分界点
        • 在发射阶段,取值和解码阶段的乱序执行很难实现也没有必要,所以解码之后的发射阶段就是两者的分界点;
      • 顺序执行
        • 每个FU都需要经历相同周期数的流水线(也就是和最复杂的那个一样)
        • Scoreboard
          • 负责记录指令的执行情况(执行过程中所有的信息),通过每过一个周期把某个值后移一位,表示指令执行到FU的哪一个阶段(3:第一个阶段,0:执行完了,写回阶段);
      • 乱序执行
        • 发射阶段,所有指令进入发射队列,只要准备好了就可以进入对应FU(实际上有个选择器(仲裁电路(select)),进行最优选择);
        • 每个FU都有自己的执行过程和执行周期数,只要执行完毕,就可以进入写回阶段
        • 写回阶段到真正的写入存储器需要在提交和退休阶段完成
        • 提交阶段(commit & retire)
          • ROB(重排序缓冲)
            • 将乱序的指令重新排序
            • 处理异常
          • SB(store buffer)
            • 缓存store的数据,等待没有异常,指令退休之后,再执行写入存储器;
          • 注:
            • 一条指令只有等到本来在他之前的所有指令都离开重排序缓存之后才可以离开,退休,并且对处理器的状态进行修改,一旦退休,指令就再也回不到之前的状态;
            • 不管在乱序执行的那个阶段(流水线)出现异常,都必须在重排序缓存里处理,因为只有这里是顺序的;
            • 指令在寄存器重命名之后,就会进入发射队列,同时也会写入ROB;
        • 乱序执行的相关性就是指目的寄存器和源寄存器之间的关系;STR同一个地址是不行的,load的乱序指令看清况;
  • Cache
    • L1 Cache(KB)
      • I-Cache
        • 指令Cache
      • D-Cache
        • 数据Cache
    • L2 Cache(MB)
      • 数据和指令还是在一起存着
    • Cache组成部分
      • data
        • 保存连续地址的数据
      • Tag
        • 保存连续地址的公共地址
    • Cache的组成方式
      • 直接映射
        • 寻址方式:index -> Tag -> Block offset
      • 组相连
        • 寻址方式:index -> Tag -> Block offset
        • 现在常用的
      • 全相连
        • 寻址方式:没有index,直接在整个Cache里进行Tag的比较,然后根据偏移确定具体位置;
        • TLB
    • Cache写入
      • I-Cache的自修改
        • 本就指令Cache就是只读不写的,所以其的自修改不会直接在其上修改,通过修改D-Cache,然后D-Cache向下修改下一级Cache,之后置I-Cache的内容无效,然后再次用到的时候,用的就是修改之后的指令;
      • D-Cache的写入
        • 写通
          • 将这个改变的内容立刻写到下一级存储设备中
          • 是有在一级和二级之间会这样干,因为速度可以,利于保持一致性;
        • 写回
          • 替换的时候再写回下一级存储
          • 因为访问你的时候会直接访问我,不会越过我(你在我里面),所以我就等到改的多了,你不在我这的时候,我在写回你那;
          • 现在只有写回,没有写通;
    • Cache的性能提升
      • Victim Cache
        • 保存最近被踢出Cache的数据
        • 就是TLB
      • 预取
        • 系统刚启动的时候,预测提取内容到Cache;
  • 虚拟存储器
    • 地址转换
      • 注:
        • 物理存储器的大小不能超过处理器的最大可以寻址空间,比如:32位的处理器,最大也就4GB,超过的处理器也找不到;
        • 虚拟地址用页式存储进行划分,划分为页,物理地址也会进行相应的划分,划分为frame;物理地址和虚拟地址的大小划分必须是一样的;
        • 因为划分一致,所以不用管偏移量,实际虚拟地址到物理地址的映射就是VPN到PFN的映射;
        • 每个程序(准确的说是进程)都会有一个页表,也就是虚拟地址和物理地址的转换;即使两个页表里的虚拟地址一样,MMU也会映射成不一样的物理地址;不过如果几个虚拟地址对应的都是同一个函数,那么MMU会把不同的虚拟地址映射到相同的物理地址;(函数就在那存着,就那一个物理地址,想整俩也没有)
      • 单级页表
        • 页表寄存器
          • 存放当前正在运行的程序的页表的起始地址(首地址)
          • 通过首地址访问页表,使用这种方式的前提是页表是连续存储在地址空间的(不连续的话,就得用链表实现,或者再搞个表格记录)
        • 真实的寻址页表,其实不是整个虚拟地址的位数,而是只是VPN,从页表中找到的物理地址也是一样,只是PFN;
      • page fault
        • 实际的地址转换中,如果页表里找不到虚拟地址对应的物理地址,会出现异常,然后处理器去处理这个异常,处理过程:在页表里找一个空着的,找不到就找个不怎么用的进行覆盖,然后把这个物理地址的对应添加进来;
        • 覆盖的时候会去看这个PTE中的脏状态位,1:被修改过,需要先写回在覆盖;0:没有被修改过,直接覆盖;
        • 替换算法:首先会去检查对应的PTE,查看使用位;
      • 多级页表
        • 各级页表之间是一个动态的过程,按需分配;
        • 前面级的PT存的是子页表的首地址;最后一级的PT才存的是虚拟地址到物理地址的映射;
        • 页表的存储还是连续的,只不过页表之间的存储是不连续的;
      • 注:
        • 单级页表的最大缺点,就是浪费内存资源,因为他提供的是个固定的页表大小,比如:4MB,而不管进程能不能用的到这个4MB所对应的4GB的虚拟存储空间;(32位处理器,页大小是4KB,则页表里要存的表项个数就是4GB/4KB,则页表大小是4B*表项个数,就是4MB,只有页表存储所有的表项个数,才能映射到整个4GB的内存);
        • 问题
          • 为啥32位处理器的页大小是4KB?
          • 人为规定的,这个值效率高而且占用内存小。你搞个页表,就得给他定义结构,他记录这,记录那,就得占内存,所以页表项就这么大(4字节)的时候,页大小就4KB的时候最好。
    • 程序保护
      • 利用虚拟存储器可以实现管理物理内存的权限;
        • PTE里会有AP控制权限,除此之外还有很多机制;
      • 操作系统一般不会使用页表,而是直接可以访问物理内存,在物理內存中有一部分是专门给操作系统使用的,别的都不可以访问;(操作系统就这一个,给他直接使用物理地址,提前分好,反而是更方便的)
    • TLB
      • 简单理解,就是页表的一个Cache;实现和功能几乎一摸一样;一级TLB也是“哈佛结构”,全相连,二级和Cache一样,存在一起,组相连;
      • TLB又加了一个Cache
        • 就是TLB和存储在內存的页表进行数据传送还是满,就又加了一个Cache的机制;
      • TLB机制解决
        • 一个是不够快
        • 一个是太小
    • 注:
      • 现在使用的虚拟地址和物理地址的映射是多级页表,而且是超多级页表;
      • 现在我们能看到的地址,全是虚拟地址,即便是链接之后的地址,也依然是虚拟地址;
  • 分支预测
    • 概述
      • 分支指令的两个要素
        • 方向
        • 目标地址
          • 直接跳转(目标地址是立即数,编译之后就只知道的)
          • 间接跳转(跳转地址是运行时才能知道的)
            • 寄存器是32位,则目标地址是32位,则可以跳转到处理器的程序存储的任意地方(处理器是32位的)
            • 间接跳转一般就是CALL/Return,为了方便实现分支预测;
      • 静态预测
        • 流水线级数不深(5级都不算深,额。。理解错了,这个5级指的不是深度,而是指令流水线的五级(取码,译码~~))
        • 默认分支就是不跳转的,如果跳转那就把挑转之前的指令抛弃,其实就是跳转的这一个周期内的,MIPS会用一个分支延迟槽的机制,在分支的这个周期执行一个不相关的指令;
      • 动态预测
        • 现在,只要预测就是动态预测;会根据指令执行之前的执行状态预测接下来的执行;
      • 识别分支指令
        • 最好的时机:取指的同时进行分支预测
        • 使用指令的PC值进行识别
          • 对于一个条指令来说,它的物理地址是会变化的(取决于操作系统怎么放),而他的虚拟地址,即PC值是不会变化的;一个人进程内,每一个PC值对应的指令是固定的,不可能出现一个PC值对应多个指令的情况;
          • 分支指令执行完,第二次再执行时,就知道现在取的这个指令是分支指令;
          • 问题:
            • 物理地址分配完不就固定了吗?难道还能变来变去的?
            • 解决:
              • 编译之后,指令的地址分配就OK,但是是虚拟地址,至于真实的物理地址,每次运行程序,都是操作系统分配的,看那块没人用,就分配哪里。
      • 哈希算法
        • 任意长度的输入,得到固定长度的输出,而且要短的多;
    • 分支指令的方向预测
      • 基于两位饱和计数器的分支预测
        • 就是当一条分支指令连续两次执行的方向都一样时,该分支指令的第三次方向还是同样的,如果一条分支指令只是偶尔变化了一次,那么这个分支指令的预测值不会马上发生改变;相当于有了一个延迟机制,过滤掉不连续的偶尔一次改变;
        • 使用PC值的一部分保存PHT(每一条指令对应的两位饱和计数器,实际只有分支指令有)
        • 别名(重名)
          • 因为PHT需要的存储空间太大(每个分支指令都有一个两位饱和计数器),芯片内部不好实现这个存储器,而且只有分支指令需要这个,所以就把PHT存储在PC值里,寻址方式造成了一个问题:k部分相同的一系列PC值用的是同一个两位饱和计数器;
        • 缺点
          • 对于变化频繁的分支指令,这种方法相当的糟糕
      • 基于局部历史的分支预测
        • 就是每条分支指令都有一个BHR(局部历史寄存器)和一个PHT;BHR里存的是这个分支指令历史记录(存多少取决于BHR的宽度),PHT里会有四个表项(00,01,10,11),通过BHR记录的历史记录,预测接下来的动作,访问PHT对应的表项,执行相应的方向;相当于两者结合着来;
        • 别名(重名)
          • 同上;
        • 缺点
          • BHR的宽度有限,对于规律周期特别宽的,就歇菜咯;
      • 基于全局历史的分支预测
        • 这个分支指令的执行和前面的另外一条分支指令的执行结果有关,比如多分支if语句;GHR(全局历史寄存器)
      • 竞争的分支预测
        • 自适应分支预测,根据实际情况选择是用基于局部历史的分支预测还是基于全局历史的分支预测;(还是有个两位表示状态的机制)
        • 由choice prediction根据CPHT(choice PHT)进行选择是基于局部历史还是基于全局历史进行分支预测;
    • 分支指令的目标地址预测
      • 直接跳转的分支预测
        • 在取到这个分支指令的时候,就把用立即数表示的那个目标地址存下来,然后根据分支的方向预测进行操作;多个PC值的目标地址存在一个共同的空间BTB(Branch Target Buffer),BTB就是一个cache,还是组相联的;
      • 间接跳转的分支预测
        • 间接跳转一般就是CALL/Return,针对这个有一套东西,如果是其他的,有专用的一套东西;
    • 超标量处理器的分支预测
      • 分支预测一套极其复杂的机制,任重道远吧;
    • 注:
      • 跳转指令的执行成功还是失败,得等到运行结果出来之后,才知道,也就是那个分支指令执行的周期结束之后;
  • 指令集
    • 复杂和精简
      • 新奇的表述:指令集是处理器使用的语言
      • RISC复杂指令集,的复杂原来是一条指令包含的信息太多,一条指令需要进行很多操作;也就是说很多情况下同样的操作精简指令集的指令数反而更多,分工产生效能吧;
      • 复杂指令集会包含很多复杂操作的指令,精简指令集则没有,精简指令集的复杂操作是通过子程序的方式实现,也就是软件的方式实现;其实现在的复杂指令集在最后也会尽力转化为精简指令集,比如X86;
      • 本质区别:
        • 是否对指令做分类和约束,精简指令集规定,所有的运算指令都是对寄存器运算,所有的访存都通过专用的指令load/store进行。
    • 精简指令集
      • ARM:一个比较像复杂指令集的精简指令集,一个指令中尽量做更多的任务;
      • MIPS:一个纯粹的精简指令集,能不要的都不要,复杂的都留给软件实现;
      • RISC-V:也是一个纯粹的精简指令集,不过比较新,更加模块化;
    • 三个精简指令集的相同点
      • 指令都是32位等长的(不用管那几个特殊的16位的,在处理器内部都会转换为标准的32位);
    • 三个精简指令集的不同点
      • load/store
        • ARM的load/store指令支持访存地址的自动自加自减,对于连续空间好用;MIPS和RISC-V不行,他俩的load/store就干一件事,所以访存地址的自加自减需要额外的操作进行;
        • ARM有多寄存器传送指令,用一条指令将存储器的一片连续地址空间的数据加载到多个寄存器中;MIPS和RISC-V不行;
      • 加减法
        • MIPS
          • 有符号加法ADD:加法发生溢出,ADD指令会产生一个异常进行处理;
          • 无符号加法ADDU:ADDU不会关注这个溢出,不会产生异常
        • RISC-V
        • ARM
          • 通过状态寄存器CPSR存储结果的状态进行处理,直接实现了带位的加法(ADC);
      • 寄存器
        • ARM里的有些特殊寄存器都是通用寄存器,指令可以直接使用,MIPS的很多特殊寄存器不是通用,指令不可以直接使用;比如:PC寄存器;RISC-V和MIPS一样;
        • 注:
          • 现在ARM的使用的特殊寄存器不再是通用寄存器了,就是通用寄存器;
        • ARM指令集的每一个指令都可以条件执行(主要表现在对状态寄存器的使用),这就使得每个指令都需要一个4位的条件码,那么指令用来寻址的位就少了(4位);所以ARM的通用寄存器只有16个,MIPS和RISC-V都有32个(5位);
      • 移位
        • ARM没有专门的移位指令,移位操作和运算操作是集成在一起的,执行运算之前就自动完成移位了;MIPS和RISC-V是有的;
    • 异常的新理解
      • 对于大多数异常来说,从其对应的异常处理程序返回的时候,都需要重新执行这条产生异常的指令。也就说开始处理异常时存的PC值不是下一条指令的,就是这一条指令的;
        • 就好比,你去找小明让他办个事,他说办这个事出现的问题,得先把这个问题处理完才能给你办这件事,那把问题处理完了,你可不能忘了你原本过来找小明是干嘛的!
      • 专门用来产生异常调用系统某些程序的,那就是从下一条指令开始了;
      • 异常处理时,对于PC值的存储,复杂指令集存在堆栈里,精简指令集存在对应的寄存器里;
      • 异常过程中,通用寄存器值的保存都是保存在堆栈的;精简指令集的堆栈指令使用一个寄存器模拟,但是实际这个寄存器和通用寄存器没去区别,指针的增减是用软件控制的;复杂指令集用的是专用的堆栈寄存器,并且有专用的入栈出栈指令,也就是硬件控制的;
    • 问题:
      • store指令为啥没有分有符号和无符号?
      • 解决:
        • 因为把数据写入到内存,我只管写到什么地方,放好;至于这个数据是有符号还是无符号我不管,以后处理器把这些数据是当成有符号还是无符号那是他的事,我也管不着,反正数据的都是一堆0110;
  • 指令解码
    • 每个指令都需要一个完整的解码电路,所以对于一个每周期可以解码n条指令的超标量处理器来说,需要有n个解码电路,并行处理;
    • 指令缓存(FIFO)
      • 加入了一个缓冲的机制,使得取码和译码更加的灵活,而且在译码出现问题(比如分支预测)的时候,流水线也不至于立即停下来;
    • 精简指令集的一些另类的复杂一点的指令的处理
      • (AMD的LDM/STM,前/后变址指令,MIPSD的乘法,乘累加)
      • 核心:把复杂的指令拆解成多条普通的指令,然后执行;
    • ARM和MIPS的对比
      • ARM的一条指令需要MIPS的两条指令才能完成,所以指令密度更高,占用的指令存储器空间肯定更小,主要反应在I-Cache的缺失率会更小,因为可以存的更多;但是因为复杂一点,所以硬件实现肯定更困难,功耗更大;
  • 寄存器的重命名
    • 相关性
      • 数据相关性
        • 寄存器重命名来解决
      • 控制相关性
        • 分支预测解决
      • 存储相关性
      • 结构相关性
    • 逻辑和物理
      • 之前接触到的寄存器其实都是逻辑寄存器,是指令集定义的逻辑上的寄存器,真正使用的是处理器上的物理寄存器(实际存在的寄存器);物理寄存器是要比逻辑寄存器多的,这样就可以实现寄存器的重命名;
    • 寄存器重命名的方式
      • 本质:寄存器的重命名的时候,动态的将逻辑寄存器映射到物理寄存器;
      • 使用ROB(重排序缓存)实现
        • 把物理寄存器(PRF)和ROB集成到一起;使用ROB对ARF进行扩展;
        • 当一个指令被写进ROB中的一个表项(entry)的同时,这个表项在ROB中的编号就是这个指令的目的寄存器对应的物理寄存器;逻辑寄存器和这个编号建立了映射关系;
        • 优点
          • 简单,不需要管理,指令写入ROB的时候,寄存器重命名就ok了;
        • 缺点
          • 每一个指令在表项中都有一个物理寄存器,也就是说即使一条指令没有目的寄存器,他在ROB中对应的表项里依然有那个不会被用的物理寄存器;
          • 对于一条指令来说,源操作数可以从ROB读取,也可以从ARF读取,这样在一个周期内 指令的所有指令都需要读取ROB或者ARF,这一个或就是很多的可能性;
          • 寄存器的值需要被改两次,有再次移动;一个寄存的生命周期,会有两个地方存放;一次是存到ROB,一次是存到ARF;
      • 将ARF拓展进行寄存器重命名
        • 是基于ROB进行的改进,把PRF独立出来;使用(增加)一个独立的存储部件,用来存储流水线中所有指令的结果,只有那些存在目的寄存器的指令才会占据这个存储部件当中的存储空间(也就是物理寄存器);PRF
        • 缺点
          • 寄存器的值需要被改两次,有再次移动;一个寄存的生命周期,会有两个地方存放;一次是存到PRF,一次是存到ARF;
          • 对于一条指令来说,源操作数可以从ROB读取,也可以从ARF读取,这样在一个周期内 指令的所有指令都需要读取PRF或者ARF,这一个或就是很多的可能性;
        • 优点
          • 相对而言还是比较简单的;
      • 使用统一的PRF进行寄存器重命名
        • 把PRF和ARF进行合并,合并之后统一就叫PRF;
        • 需要两个RAT
          • RAT(重命名映射表)
          • 一个记录逻辑寄存器到物理寄存器的映射
          • 一个记录所有退休状态的指令和物理寄存器的映射
        • 缺点
          • 复杂,需要一个(free list)存所有空闲物理寄存器的编号,两个RAT;实现复杂,控制复杂;
        • 优点
          • 寄存器的值只需要被改一次,不需要再次移动;因为PRF和ARF统一,过程中也没得移,就这一个地方;
          • 一条指令的源寄存器的值只能存储在一个地方,其他两个有两个地方都有可能存储源寄存器的值;
          • 问题:
            • 不是只有目的寄存器才会寄存器重命名吗?这个源寄存器是那来的?难道是这个指令的目的寄存器重命名存储之后,变成了下个指令的源寄存器?
            • 解决:
              • 物理寄存器是实际存在的,逻辑寄存器最终使用的就是物理寄存器,所以使用重命名之后,其实很多(大于逻辑的)的物理寄存器里都是存储值的,这个时候,指令的源操作数到底来自那个,就不一定了;
          • 只有当一条指令退休的时候,他对应的物理寄存器的状态才会标记为指令集定义状态,才可以被外界访问;
      • 重命名映射表(mapping table,RAT)
        • 记录着逻辑寄存器存储的位置,比如是ROB还是ARF,是PRF还是ARF,也就是具体的映射关系;
  • 发射
    • 发射(issue)
      • 就是将符合一定条件的指令从发射队列(Issue Queue)选出来,并送到FU中执行的过程;
      • Topic1

      • 发射队列(Issue Queue)
        • 使用硬件保存一定数量的指令,然后从这些指令中找出可以执行的指令,而不管指令之间的原始顺序;也别成为保留站;存储着进行寄存器重命名之后的,并且还没有被FU执行的指令;
      • 分配电路(Allocation)
        • 把进行过寄存器重命名之后的指令写入发射队列:先从发射电路中找到空闲的entry,然后再写入;
      • 选择电路(Select)/仲裁电路(Arbiter)
        • 按照一定的规则(旧的先上),对源操作数都准备好了的指令进行合适的选择,然后送到FU执行;
      • 唤醒电路(Wake-up)
        • 再FU执行指令得到结果之后(实际中可以提前),向发射队列中所有等待这个数据的指令进行通知(就是广播),然后收到的指令会和之这个值进行比较,看是不是自己等的源操作数,是的话就设置状态为准备好状态(ready),并且这个指令会向仲裁电路发出申请;
    • 发射队列的实现方式
      • 集中式VS分布式
        • 集中式(CIQ):所有的FU都共用一个发射队列
          • 容量大,利用率高
          • 选择电路,唤醒电路变得复杂,面积大,延时高;因为选择面对的数据多;
        • 分布式(DIQ):每个FU都有一个单独的发射队列
          • 容量小,选择和唤醒电路实现简单,利用率低
          • 如果一个发射队列满了,整个流水线都得停下来;因为一个发射队列满,后续的指令寄存器重命名都没法进行,即便别的发射队列有空闲空间,也没用,都得死!
        • 对比:一般都是根据实际情况,结合着用;
      • 数据捕捉VS非数据捕捉
        • 在流水线的发射阶段之前读取寄存器(数据捕捉data-capture)
          • 指令在进行寄存器重命名之后,首先先读取物理寄存器堆(PRF),然后把读取到的值一起送入发射队列;如果指令中的寄存器中的值还没有计算出来,那就把寄存器的编号一起送进去,同时标记这条指令是当前无法获得状态(non-available);
          • payload RAM
            • 负责存储源操作数,并捕捉对应FU的结果;
            • 当一条指令被仲裁电路选中的同时,他会将他的目的寄存器的编号进行广播,其他还在发射对队列的指令会把自身的源寄存器和这个广播编号进行比较,一旦发现相等的情况,就会在payload RAM对应的位置进行标记,那个选中指令在FU执行完毕,就会把他的结果写到payload RAM这些(可能很多)被标记的位置;这个写入是通过旁路网络(bypassing network)来实现的,原本的需要源操作数的指令不需要访问物理寄存器堆;
          • 发射队列负责比较寄存器的编号值是否相等;
          • issue width(发射的宽度)
            • 每个周期最多可以发射的指令的个数(也就是有多少个FU)
          • machine width
            • 每周期实际可以解码和重命名的指令的个数
        • 在流水线的发射阶段之后读取寄存器(非数据捕捉non-data-capture)
          • 指令进入发射队列的时候带着的都是源寄存器的编号,只有被仲裁电路选中之后,才使用源寄存器的编号来访问物理寄存器堆,带着读取的值进入FU;所以这个方式不需要payload RAM;增加了处理的速度;
          • 由于是在发射之后读取寄存器的值,多以寄存器堆的读端口数是issue width*2;这个很明显不小,所以需要寄存器堆有很多的读端口;
        • 对比
          • 数据捕捉需要的读端口数少,但是因为源操作数存在发射队列中,则发射队列的面积就大,而且多了很懂数据的的读取和写入过程,耗能就大;非数据捕捉,读端口要求多,速度快,耗能小;
        • 注:
          • 主要的区别就是,进入发射队列时带不带指令的对物理寄存器的值;
      • 压缩VS非压缩
        • 压缩发射队列
          • 有个自动压缩(补位)的机制,走一个,上面的全部下一个;所以旧的指令永远在下面,新的指令永远从最上面写入;
          • 优点
            • 分配电路实现简单,直接写入到最上面就行;
            • 仲裁电路实现简单,已经按照最新->最旧排列好,直接用就行;
          • 缺点
            • 实现起来浪费硅片资源,自动压缩的机制,如果每次取两个,三个需要复杂硬件(多路选择器和布线);
            • 功耗大,动一个,其他的很多都要动;
        • 非压缩发射队列
          • 有指令离开的时候,其他的指令不动,保持原位;这样有指令进来,就需要先找空位;
          • 优点
            • 功耗小,面积小
          • 缺点
            • 分配电路和仲裁电路实现复杂
    • 发射过程的流水线
      • 指令进入发射队列要被FU执行需要满足的条件
        • 这条指令所有的源操作数都准备好了
        • 被仲裁电路选中
        • 能够从寄存器,payload RAM,或者旁路网络中获得源操作数的值
        • 注:
          • 这三个条件是顺序发生的
      • 非数据捕捉结构的流水线
        • 最简单的方法
          • 当一条指令在FU中得到结果之后,再进行广播,将发射队列中需要用到这个结果的指令对应的源寄存器的状态设置为准备好状态;
        • 实际中可以利用旁路电路把唤醒过程提前
          • 当一条指令开始执行之后,就可以对相应的指令进行唤醒;反正我开始执行,结果就在后面,你只需要比我慢一步就可以;
          • 背靠背的执行
            • 仲裁和唤醒是在同一个周期内出串行执行的,也就说他俩合起来是一个原子操作;
            • 这里就存在一个问题,如果前一个指令需要不止一个周期才能执行完成呢?(比如乘法和一些特殊的load指令)
      • 数据捕捉结构的流水线
        • 和非数据捕捉差不多,加入了一个payload RAM,指令获取源寄存器的值,不需要对物理寄存器堆进行访问,而是通过payload RAM或者旁路网络;
        • 仲裁和唤醒依然是在一个周期,可以把payload RAM也放到一个周期,但是很单独放到一个周期执行更好;
    • 分配
      • 压缩方式的发射队列的分配
        • 只要发射队列有空位置,直接放到最上面就行;
      • 非压缩方式的发射队列的分配
        • 需要在发射队列设置一个空闲标志信号(free),分配电路可以在整个发射队列去找空闲的位置,这种延迟慢;也可以把发射队列分区,分别在各个区里找一个空闲,这种有个严重的问题,一个区满,整个流水线都给停,也就是没法把指令写入发射队列,因为在寄存器重命名之后,发射之前,指令还是顺序执行的;
    • 仲裁
      • 压缩方式的发射队列的仲裁
        • 因为已经是最新->最旧排序好的,所以直接从最旧选择就可以;
        • 仲裁电路的选择机制,就是最旧的先选择;因为越旧的指令,他和别的指令的相关性越大,占用的资源也越多,所以往往优先执行最旧的指令是最好的;
      • 非压缩方式的发射队列的仲裁
        • 发射队列里的指令的位置是完全随机的,这就需要仲裁电路对发射队列的指令进行识别,那个是旧的,那个是新的。
        • 指令在寄存器重名之后,会写入发射队列,同时也会写入ROB(重排序缓存),这个写入的时候,指令还是顺序执行的,所以ROB里其实存储着所有的指令,而且还是顺序执行的;
        • 因为ROB本质上是一个FIFO,所以没法直接按照位置判断指令的新旧,加入和一个位置值(position bit)(标志写指针和读指针的关系,是不是在一个面)
          • 位置值相同,ROB的地址值越小,对应的指令越旧;
          • 位置值不同,ROB的地址值越大,对应的指令越旧;
        • 这样,把ROB的位置值和地址值一起和指令送到发射队列里,然后仲裁电路就可以进行判断选择;但是还是有很多问题,没准备好的指令咋个处理?发射队列里现在是存着年龄信息,咋选?——通过复杂的电路实现;
        • 1-of-M的仲裁电路
          • 就是上面的说的一堆,一次只选择一个指令;
        • N-of-M的仲裁电路
          • 就是一次选择N个指令,放到N个FU去执行;
          • 一种思路:一级电路选完第一个最旧的之后进行标记,然后再来一个1-of-M二级电路再剩下的里面选择最旧的,很明显实在太费资源;
          • 折中思路:用类似多路分配器,实现一个不是理想的N-of-M的仲裁电路,比如,本来设计的是4-of-M仲裁电路,但是有时这个电路只能实现3-of-M;
    • 唤醒
      • 唤醒的广播机制是通过总线的形式实现的;
      • 单周期指令的唤醒
        • 被仲裁电路选中指令会把他的目的寄存器的编号,送对对应的总线上;
        • 每一条总线上的值会和发射队列中所有指令的源寄存器的编号进行比较,如果发现相等,就会把自己对应的源寄存器的状态设置为准备好状态;
        • 发射队列中的某条指令发现自己所有的源寄存器的状态都是准备好的,并且他还没有被仲裁电路选中,那他就会向仲裁电路发出申请(选我!选我!)
        • 如果仲裁电路发现,当前有优先级更高的指令,就不会理睬这个指令的申请,下个周期这个指令继续向仲裁电路发出申请;如果这条指令一直得不到这个仲裁电路的回应,他就会心灰意冷的去找另外一个(如果另外一个存在的话);如果得到了回应,这条指令就会开心的把自己标记为被选中状态(名花有主);为啥不直接发射(嫁过去),因为考虑到有些指令需要好几个周期才能运行完,所有需要在这等等,有个机制待处理;
        • 这条指令根据得到的回应,把他的目的寄存器的编号,送对对应的总线上,同时去FU执行;
      • 多周期指令的唤醒
        • 延迟广播
          • 指令被仲裁电路选中之后,延迟相应的周期广播,也就是延迟相应的周期把目的寄存器的编号送到总线上;这种情况可能会出现延迟的广播,和现在不延迟的广播在同一条总线上的冲突;问题的解决可以增加总线的个数,也可以用个表记录需要广播的情况;
        • 延迟唤醒
          • 指令被仲裁电路选中之后,广播正常,把比对进行延迟;
      • 推测唤醒
        • 就是很多指令的运行周期,是不知道的,没标识出来,所以需要推测一下;
  • 执行
    • FU的类型
      • ALU(Arithmetic and Logic Unit)
        • 整数的加减,逻辑,移位,有些会有简单的乘除,数据传输指令(MOV指令),或者其它的数据交换(byte-swap),分支指令的目标计算,访存地址的计算;(不过超标量为了追求性能,是不会这样干的)
      • AGU(Address Generate Unit)
        • 用来计算地址,计算load/store指令中的地址;
      • BRU(Branch Unit)
        • 负责处理程序控制流类型的指令;分支,跳转,子程序调用,子程序返回;负责将指令携带的目标地址计算出来,并根据一定的条件来决定是否使用这些地址;
        • 负责对分支预测的正确与否进行检查,发现预测机制,就要启动恢复机制;
      • 注:
        • 通常为了性能,会有多个ALU,AGU,多个BRU,或者其他的FU;
        • 超标量寄存器中,也需要对CPSR(状态寄存器)进行寄存器重命名,当成通用寄存器操作;
    • 旁路网络
      • 由连线和多路选择器组成;从FU输入端到输出端之间的一个通路;负责将FU的运算结果马上送到他需要的地方;
      • 目的是为了获得更高的并行性;
    • Cluster
      • 是一种方式,把负责的一个东西,给分成几个简单的并行处理,增加处理的速度;
  • 提交
    • 一个从乱序又回到顺序的过程
    • 超标量处理器的状态
      • 指令集定义的状态(Arichitecture State)
        • 通用寄存器的值,PC的值,存储器的值;
      • 超标量处理器内部的状态(Speculative State)
        • 从寄存器重命名开始到提交之前的,都是这个状态;
    • SOB存储两个主要信息
      • 指令的信息,和指令执行之后的结果
      • 注:
        • store这个指令在SOB和SB里都有;
    • 分支预测失败的恢复机制
      • 指令从SOB退休之后的提交问题
        • 退休时,ROB中的每条指令必须检查自身的映射关系是不是最新的;因为如果有好几个指令的目的寄存器都是R1,那明显最后一个执行的,也就是最后一个进入SOB是指令是操作R1的最新值,退休的时候,就没必要写入前面的几个指令的R1值了;
        • 注:
          • 上面提到的这个几个指令,都是一起在SOB中的,也就是第一个退休的时候,其实SOB例就存在第二,第三个,后面的指令;
        • 判断SOB指令的映射是不是最新的
          • 不是很明白,大概是找到这个指令的目的寄存器,然后让目的寄存器读取PAT,然后获得一个值,比较一样不一样;
      • 基于ROB进行寄存器重命名架构的恢复
        • 预测失败时,已经在执行的指令继续执行,停止取指令(流水线抽干);然后等到这个分支指令之前的所有指令(包括他自己)都退休之后,此时抹掉现在在流水线上的所有指令(因为他们都是错误的),此时ARF中的所有寄存器都是正确的,然后将RAT中的所有内容标记为哦APF状态,就欧克;
        • 问题:
          • 此时ARF中的所有寄存器都是正确的?
      • 基于统一的PRF进行重命名的架构的恢复
        • 有两个RAT
          • 前端RAT(Speculative RAT)
            • 在寄存器重命名阶段使用
          • 后端PAT(Architecture RAT)
            • 在提交阶段使用
            • 所有从流水线退休的指令,如果他存在目的寄存器,都会更新这个表格,因此他永远是正确的;可以使用他进行分支预测失败之后状态的恢复;
        • 过程和上面的一样,就是最后是把后端RAT复制到前端RAT实现状态的恢复;
        • 问题:
          • 和上面的一样,为什么流水线抽干之后,后端PAT一直是正确的;
    • 异常
      • 需要记录所有指令发生的异常,让后按照指令的原始顺序进行所有异常的处理;很明显,这个需要ROB处理;
    • 中断
      • 马上处理
        • 把流水线上的指令全部抹掉,处理中断,然后再恢复状态;
      • 延迟处理
        • 已经再流水线上的指令,先执行,退休之后,再去处理中断;
    • Store的处理
      • 使用SB之后,load指令需要再D-cache和SB里同时查找;发现store指令的地址和自己相同,并且store在自己之前进入流水线,那么就是你了,你的数据load就直接拿来用了;
      • store这个指令在SOB和SB里都有;SB是因为store从退休到数据写到存储器,一旦发生D-cache的确实,占用SOB的时间过长,影响性能,所以引入了SB的机制;SB中有个状态位,对store进行标记,一旦store退休,立马离开SOB,后面发生任何事都是SB负责,解放SOB的空间;然后只有store把数据真正的写入D-cache成功,才可以离开SB;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值