流水线杂谈

前言:

前段时间看了《深入理解计算机系统》的处理器章节,对流水线的实现有一点点的理解,随便写写这些,不过这些是基于书中自建的y86处理器,细节或部分结构和x86有区别,不过应该思想上差不多吧。

一.处理器是干啥的

先说说处理器是干啥的吧,其实就是处理指令的。我们平时写的各种编程语言比如C,C++经过编译器编译生成 .s 汇编语言程序,汇编语言更接近机器语言,每一条指令都可以翻译成一段长度不等的01机器序列,汇编器就是将生成的 .s 汇编程序翻译成机器可以识别的机器语言程序 .o文件的。最后再经过链接器链接各个 .o 文件,就可以生成可执行目标文件 .exe 了。处理器就是处理最终的01序列的。可以不那么复杂,就将处理器看做是可以一条条执行汇编指令的一个硬件结构吧。

二.指令结构

处理器要处理指令,必然要有一种方式去识别指令。这里要说一下汇编指令翻译到机器代码的结构了。一个汇编代码,翻译到机器代码由指令指示符,寄存器指示符,常数字组成。

1.指令指示符:

指令指示符由指令代码和指令功能两部分。你可以认为指令指示符就是一个给指令编码的字节数字,用于区分不同的指令(毕竟机器不能认识字母什么的,它只能认识数字)。在编码的时候可以发现有一部分汇编指令功能很相近,比如说关于操作的指令:add,sub,and,xor,或者是关于分支关于传递的一些指令。可以将这些指令分为一组,再用数字给每一组中的指令编号。我想分组的好处就是后来在处理器识别指令的时候,这一部分类似的指令只在硬件实现上有细微的差别,可以按一组处理,再做细微调整,不用再去费力重复逻辑。说回指令指示符的两个部分。指令代码相当于组号,每一类不同的指令有不一样的编码。指令功能相当于每一组中指令的编号,若该组只有一个指令则是0。两者搭配使用就可以确定一个确切的指令。y86的设计是用一个字节作为指令指示符,高四位作为指令代码,低四位作为指令功能部分。在其设计上,只要有了指令指示符部分,这条指令是做什么的就知道了,相应的是否需要寄存器,或者常数字也都知道了,指令的长度也就确定了。

2.寄存器指示符:

上面说到,知道指令指示符就可以确定是否需要寄存器,如果需要,则有寄存器指示符部分。举个例子吧:rrmovq  rA  rB  指令将寄存器 rA 的值移送到 rB 寄存器中,很明显,这条指令需要两个寄存器;pushq rA 指令将寄存器 rA 的值入栈,需要一个寄存器;ret 返回指令不需要寄存器。需不需要寄存器由指令指示符部分确定了,接下来需要什么寄存器要寄存器指示符来告诉处理器。同样的,这里的处理是对寄存器进行编码,用一个字节来作为寄存器指示符,高四位用于表示 rA 是哪个寄存器,低四位用于表示 rB 。对于不用寄存器的没有寄存器指示符部分;有一个寄存器的,另一个用 F 标记,意思是这里不用寄存器。

3.常数字部分:

常数字是干啥的呢?一般用于两种情况,需要立即数和需要地址的时候。下面说的是在y86的设计上的情况。

(1)irmovq V, rB   这条指令将立即数V的值移动到 rB 寄存器中,很明显,将其翻译成汇编代码的时候,我们也需要存储V的值。

我们将其翻译成机器语言:3 0 F rB V(八个字节)

说明一下:3 0 是指令指示符字节,3是指令irmovq的指令代码,即组号,由于其组下没有其他指令,所以指令功能部分用0填充;F rB 是寄存器指示符字节,没用到 rA,所以用 F 填充,rB这填的是其编号(为什么会有 rA ,rB的区别呢,我想它是将 rB 作为最终写入的寄存器);y86的处理器设计比较简单,默认数据都是八个字节长度,所以一个立即数需要后面的八个字节来存储。总共这条指令占10个字节长度。这10个字节的01信息,就足以告诉处理器如何处理,以及提供给它需要处理的数据信息。

(2)rmmovq rA , D(rB)      这条指令将寄存器 rA 的值写入内存中。值得注意的是这里选择的方式是相对寻址,即绝对地址+地址偏移,需要放到 rB 寄存器中装的地址数 + 地址偏移数 D 所在的内存地址里(至于为什么选择这样的方式而不用绝对寻址我还不理解)。所以 D 也是我们必须提供给处理器的数据信息。

同理有   mrmovq D(rB)  rA  。

(3)call Dest   这条指令使控制转移到地址Dest处,就像函数调用,下一条指令要到调用函数处。也需要向处理器提供地址的数据信息。注,这里用的是绝对地址。

同理有  jxx Dest   条件跳转语句。

好了,知道指令结构了,那么拿到一条汇编指令,我们就可以将它翻译成机器代码,即一串01序列,这也是处理器得到的信息。

三.指令处理

拿到这些指令信息后,处理器要怎样处理,需要我们了解一条指令的实现需要经历哪些步骤。前人总结,不管哪个指令,要经历的处理过程无外乎取指,译码,执行,访存,写回,更新PC。

举个例子吧:

rrmovq  rA, rB   这个指令前面提到过,处理它的过程如下。首先取出这条指令,同时因为知道了指令长度,可以预测下一条指令地址即  valP = PC + 2;译码阶段就是将寄存器 rA 的值从寄存器中读来即 valA ;执行阶段,因为它是直接将 rA 的值放入 rB ,所以它执行了 valE(从执行阶段出来的值) = valA + 0 ;访存它不需要;在写回阶段,将valE这个值写入到寄存器 rB 中;更新PC,PC = valP。

ret  取指;译码阶段处理比较巧,因为函数返回的过程需要做的事是从栈中取出之前调函数时存的其下一条指令地址,所以这里需要有出栈操作,同时后来还要操作栈指针,两个地方要使用栈指针,所以它读两次栈指针的值,分别放到 valA  和 valB 中(虽然两个值一样,但分别用于两个操作,valA 用于读地址更新PC,valB用于更新栈指针,这样做逻辑上应该会更简单吧);执行阶段,因为出栈的操作需要增加栈指针,这里执行的是 valE = valB + 8;访存阶段,我们需要读出之前在栈中存储的调用语句下一条的指令地址,所以要去访问原来栈上的数据,就需要以 valA 值作为访存的地址,访问该内存地址的数据放到 valM 中;写回,我们要更新栈指针,将 valE 的值写回到 %rsp 栈指针寄存器中;更新PC, PC = valM。

类似的,任意一个指令你都可以翻译成这个过程。在设计处理器时,首先最笨的办法是为每一个指令都设计一个硬件块,然后根据指令编码的不同送入不同的块中进行处理,这样做无疑浪费了很多的硬件资源。现在看来这个办法很笨,但是想想如果没有前人总结出来指令通用的以上几个步骤,我们很可能想到这,所以看来对事物进行剖析分类是很有意义的。现在,我们想到的办法就是设计一个硬件框架,它具有以上取指,译码,执行,访存,写回,更新PC的几个逻辑块,每条指令根据自己的需要,有选择的经过以上处理步骤,这样就只用一套硬件就可以应对所有的指令了。有些像小时候玩的做蛋糕游戏,蛋糕在传送带上流过去,需要什么就做什么,不需要就跳过,走完整个过程,什么样的蛋糕都可以完成。(当然,这个不是流水线哦,它是一个蛋糕做完再开始将下一个蛋糕放上去走流程)

这里想具体说说这几个步骤是干啥的:

取指:从PC程序计数器中读取指令的字节,PC增加器增加PC,根据指令是否需要其他字节继续该过程,可以在读出指令的同时计算valP(顺序执行下就是下一条指令的首字节地址)。

(取指这里有问题,这样的理解的话,会和程序计数器是时钟寄存器相冲突,我下来再看看,先放到这里吧)

译码:一般是从寄存器中读取数据,可能是直接指出的,也可能是暗含的例如ret。

执行:在算数逻辑单元ALU中进行计算,计算结果用valE表示。在这个过程中,会设置条件码。这里说说条件码吧。汇编代码和高级语言在这里不太一样,在高级语言中,我们会有判断语句,其在汇编代码中,是某一条语句在计算过程中发生了一些情况例如零、溢出等,设置了相应条件码,在判断时根据相应条件码和逻辑就可以验证判断(至于这个判断逻辑可以研究研究,我还没太去推)。若是含有判断的指令在执行阶段会根据条件码算出一个标志位Cnd,用于分支选择。

访存:这里访问的是内存,从内存中读或者写都在这个过程。

写回:这里指的是写回到寄存器中。

更新PC:由于控制不是一直是顺序的,比如有call,ret,jxx都有可能改变下一条指令的地址,所以需要在valP,valM,valC(常数字)中选择PC值进行更新。

到这里我们就清楚一条指令使怎样处理的了,下面就慢慢靠近正题了(不知不觉前面说了那么多hh)。

四.SEQ顺序处理器

基于上面的理解,我们想到让每一条指令都经过一遍上面的过程,一条完了接着再下一条开始,没啥问题,这就是顺序处理器。而整个过程由时序控制,每个周期执行一条指令,其可行性需要在我们了解了一些下面的跟时序有关的硬件性质以后探讨。

1.SEQ的时序(存储器与时钟知识点)

(1)首先要将其实现分为三个部分,组合逻辑和两种存储器设备(时钟寄存器,随机访问存储器)

组合逻辑:我对其理解就是一个流。怎么说这个流呢?电子设备的信息都是01,是因为其是靠电线传输信号的,那么其能传递的信号只有两种,低电平和高电平。既然是电传输,那么电线这端给了一个电流后,在某个短的时间差内就可以传输到那段,近似看做这段给,那边就出,中间加上一些与非门等做逻辑控制,就组成了组合逻辑。它是不受控的,这边有,那边出,实时变化的,不会保留状态。

时钟寄存器:程序计数器,条件码寄存器。顾名思义,时钟寄存器就跟时钟相关了,每个时钟周期会更新一次时钟寄存器的值,比如说在每个上升沿更新寄存器的值。这里可以想象成,一个块左端连了一些信号线是输入,右端连了一些信号线是输出,中间用某种机制将左端的输入给拦住了,右端输出跟之前检测到的输入保持一致。在拦住期间,左边信号改变不会影响右边,然后在时钟线号上升沿来临时,栏门打开,流入到右边,右边信号随之改变,栏门又放下。这个过程是靠时钟控制的,有了这个性质,我们就可以阻断某些过程的进展,用时钟来控制处理器的行为。另外,它是可以存储状态的,在栏门放下的期间,其保留了上一次采集到的状态。

随机访问存储器:寄存器文件,指令内存,数据内存。它们的共性就是,读的时候,只要有读的输入,随即产生读出结果的输出,不需要时钟控制,类似于组合逻辑流(所以叫随即访问存储器吧,不知内部实现差别)。但是写入需要时钟控制,和时钟寄存器一样,在每个时钟上升沿才能写入内存(我想这样设置是为了防止误写吧)。

寄存器文件:这里存储的是我们在程序中见到的寄存器例如%rdx,%rsp等,可以看到说到寄存器有两个说法,一个是硬件上的寄存器,称作硬件寄存器,一个是CPU中为数不多的可寻址的字叫软件寄存器。这里存储的是软件寄存器。其有读端口和写端口,每个端口有地址线和数据线,地址线只需要输入寄存器ID即可(就是对寄存器的编码)。

指令内存,数据内存:这两个差不多,通常有可能就是在一个硬件单元里。具有读、写控制位,地址线,数据线,数据输出,错误信号。这个形式就和寄存器文件不一样了,它读写的数据线是共用的,信号来控制是读还是写(注,这里引发思考,一个时钟周期内,同时读写会冲突,所以是不是所有指令都不会同时读写呢)。

2.SEQ实现的可行性讨论
上述内容了解了哪些部分跟时序有关,我们的指令是一个周期执行一条,那要看上述跟时序有关的寄存器是否会使指令正常执行。

我们列出被时序控制的部分:程序计数器,条件码寄存器,内存写,寄存器写。程序计数器,在每个时钟上升沿更新,刚好读入新指令,开始经过组合逻辑流,其可控制每时钟周期执行一条指令。条件码寄存器,本次指令读到的是上一次的条件码,因为本次产生的条件码,只有在该周期结束即开始下一条指令是才可以得到更新,幸好,我们没有指令会使用自己产生的条件码(不信你可以试试看,嗯我也没试,如果有人试出来不对告诉我),这是不回读原则。内存写,同理,内存写入的内容在下条指令开始时才会更新进去,不过没有指令使用自己写入的内存值,另外,上面提到一个问题是读写冲突,不过好像也没有这种情况发生。寄存器写也一样。

一条条分析排除后,就会发现这个想法是可行的。不过使用这样的框架需要足够长的周期使每一个指令的每一个阶段都被完成。至此,一个顺序处理器的想法就成立了。

五.PIPE流水线处理器

1.从顺序到流水线:

有了顺序处理器,我们发现一个问题,虽然说一条指令在一个周期流过去,看似一个整体,并不是完全按照我们前面的处理步骤依次进行。但其实有些处理过程是空闲的,如果按照我们预想的顺序的话,那么只有一个处理过程在执行,其他的都空闲着,效率很低,即使实际没有预想这么有序,但也存在这样的浪费。

所以我们想,为什么不让同一时刻每一个部分都被用上呢?这就是流水线的想法。就是将一条指令的处理过程严格分隔成取指,译码,执行,访存,写回,更新PC这几部分,使每一条指令在同一时刻只会占用一个部分的硬件资源。在一个指令从取指到执行后,第二条指令进入取指,以此类推,那么同一时刻就有6个指令在同步执行,简单天真想的话效率提高了6倍(实际很多因素不会这么理想,例如中间要加寄存器会有时间消耗,本身执行比预想的快等)。那么怎样严格分割处理过程?前面提到时钟寄存器,具有每个时钟上升沿更新寄存器的性质,可以达到阻塞信息传送的效果。那么在每个过程前都添加一个时钟寄存器,使本指令的所有信息会根据时钟来传送到下一阶段,使下一阶段为该指令工作,这样就可以达到控制指令分步处理的效果。

其实,真正在过渡的时候,y86采用的是将更新PC放到了前面,和取指合并,那么就是5个阶段,每个阶段前都加上了一个时钟寄存器,大概就是流水线的雏形。

但是这样的设计又会引发很多的问题,下文讲到。

2.流水线冒险

流水线是个好想法,但由于它一条指令还没有执行完,另一条指令就开始了,由于数据相关性和控制相关性,就会引发很多冒险冲突。典型的冲突分为数据冒险和控制冒险。有一个很重要的思想就是,有可能错误的程序不应该改变任何程序员可见状态。

(1)数据冒险:什么意思呢,就是这一条指令要在写回阶段往一个寄存器中写入值,但是它的下一条指令需要用这个寄存器的值,在译码阶段就会去读该寄存器,但是这时候,这个指令才在执行阶段,数据还没有写进去,它的下一条指令在读的时候就会出错。

例如:   rrmovq %rdx %rax

              addq     %rax  %rsi

解决办法:暂停,数据转发。

暂停:既然要等到上一条指令写回时,这条指令才可以译码正确,那我们暂停三个周期,将这条指令阻塞在译码阶段,并且让下一条指令阻塞在取指阶段,等待写回阶段完成就可以正确执行了。处理办法是,一将程序计数器保持不变,则取出的指令一直是该指令,在阻塞取指的同时,还可以让译码阶段仍是该指令,二是在执行阶段插入一个气泡,气泡类似于一个自动产生的nop指令,不会改变任何状态,这样执行阶段即以后就不会执行该指令去改变某些状态,达到暂停等待的效果。这个办法好处是可以完全解决数据冒险,坏处是浪费时间周期去暂停了。想想程序中数据冒险的地方还挺多的,都用暂停来处理,还是效率不够高。

数据转发:这个想法很巧妙,就拿上一个例子来说,在执行add指令时,我们发现我们要用rax寄存器的值,而上面指令会更新rax的值。add指令在译码时要读,同时rrmovq指令在执行阶段计算出了最终要给rax的值e_valE(这里是小e代表是组合逻辑算出的值,不需要本周期结束就可以更新),那么我们直接将这个e_valE的值给到add的valA不就好了(中途截胡),不用去读寄存器了。这样只需要加一些控制,就可以提高效率。但是数据转发不能处理加载/使用数据冒险。这种情况是说,如果本条指令需要从内存中读出数据写入寄存器,那么其数据只有在访存阶段才可以得到,下一条指令若要用该寄存器,在译码阶段是无法得到正确转发值的(至少转发值在上一条指令的执行阶段得出)。解决办法解释利用暂停加转发,在执行阶段加一个气泡,将指令阻塞在取指一个周期,然后上一条指令就会到达访存阶段,转发其访存的结果值m_valM即为所要值。

所以,在处理数据冒险时,这里采用的是转发加暂停的办法,能转发就转发,暂停打辅助。使用暂停的方法来处理加载/使用数据冒险叫做加载互锁,由于只有加载互锁才会浪费一个周期,所以整体效率还是比较高的。

(2)控制冒险:我们的程序并不是按照书写的代码顺序执行的,会有调用函数,条件分支等会改变程序的执行顺序。call命令我们在取指就可以得到下一个地址,下一次取指可以正确执行。但是ret和jxx就不一样了。

ret处理:返回指令,需要在访存阶段读出栈尾的值,作为下一条指令地址。所以,只有在ret指令结束访存进入写回阶段时,下一条指令地址才可以读出。为了保证程序正确执行。我们需要将程序阻塞在取指阶段3个周期,即在译码阶段加入3个气泡,使ret到达写回的时候开始取下一条指令。

jxx处理:条件跳转,因为条件判断需要条件码,所以条件跳转需要上一个指令完成执行阶段,产生条件码之后,也就是到访存阶段,才可以确定下一条指令地址。按和ret相同的处理,可以往译码阶段插入两个气泡。但是我们想,跳转指令的两个可能地址是在我们取指阶段就确定的了,我们不确定的是选哪个,为什么不先选一个,如果对了,不是省了暂停的时间吗。怎么选也是个技术活。分支预测策略:这里使用的是总是选择我分支,研究表明成功率大约为60%。还有一种策略是反向选择正向不选择,这是根据循环经常选择以及地址大小关系推出来的,成功率65%,稍微复杂一些,可以自己去看看书。所以,我们总是选择分支,等到上一条指令到达访存,即这条指令到达执行阶段时,即可判断是否选择有误。如果有误,这使我们已经取出了两条指令,我们需要将这两条指令消除(指令排除),即在下一条指令的执行加气泡,下下条指令的译码加气泡。还好这两条指令都没有进入执行阶段以及以后,并没有改变程序员可见状态,因此我们可以直接取消,它们就像从来没有来过一样。同时下一条取指取应该选择的分支,就回归正轨了。

结束:

嗯就说这么多吧,其实还有一些内容,异常处理,以及以上想法的控制逻辑等等,细节方面还有很多。就浅浅到这里吧。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
ava实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),可运行高分资源 Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值