天天看点

流水线学习笔记(二)

摘要: 对于偏软件的程序员,时常对指令的乱序执行,寄存器重命名,超标量处理器,等名词感到疑惑。本文将对这些知识进行初步介绍,为你解开这些疑惑。本文是我阅读Computer Architecture- A Quantitative Approach的学习笔记,文章中在原书例子的基础上,加上了我自己的一些理解。写作本文的目的是学习的总结和备忘,同时与爱好者进行交流,因此错误之处,期待各位斧正。

由于本书主要以MIPS为目标平台进行介绍的,因此例子也是MIPS汇编代码,但是熟悉X86汇编的朋友也不用担心,在文章开头,我特别对几条汇编指令进行了解释,相信汇编语法不会成为你阅读本文的障碍。

单一流水线结构在遇到资源冲突和数据依赖时,比较停滞流水线,直到依赖被解除。例如:

时钟周期
指令 1 2 3 4 5 6 7 8 9
DADD R1, R2, R3 取指 译码 执行 回写
DSUB R4, R1, R5 取指 Stall Stall 译码 执行 回写
AND R6, R5, R7 取指 Stall Stall 译码 执行 回写
......

在这种情况下,由于第二条指令的依赖关系,致使第三条及后续指令受到“无辜”的牵连。我们可以通过编译器在编译期进行依赖性检查,把指令调整为下面这个样子:

DADD R1, R2, R2
AND R6, R5, R7
......
DSUB R4, R1, R5
......
      

但是编译器取得的效果有限,为了进一步挖掘指令“重叠”执行的潜力,当没有依赖的情况下,我们希望指令按下列方式进入流水线:

时钟周期
指令 1 2 3 4 5 6 7 8 9
DADD R1, R2, R3 取指 译码 执行 回写
DSUB R4, R1, R5 取指 Stall Stall 译码 执行 回写
AND R6, R5, R7 取指 译码 执行 回写
......

从上面可以看出,第三条指令先于第二条指令结束,我们把它称为乱序执行(Out-of-order execution)。乱序执行的关键是要求指令调度单元能够动态检测指令的依赖关系,又被称为指令相关性,为了进一步介绍乱序执行的相关算法,我们先来对相关性进行总结和分类。

  • 数据相关性(Data Dependences)

    数据相关性可以细分为三类:真实数据相关性,名字相关性,控制相关性

  • 真实数据相关性(True Data Dependences)

    指令i产生一个结果,后续指令j使用该结果作为输入操作数,被称为指令j依赖于指令i。如果指令k依赖于指令i,同时指令j又依赖于指令k,那么同样可以得到指令j依赖于指令i。例如下面的例子:

    L.D F0, 0(R1)
    	; 源操作数寄存器 F0 依赖于前一条指令
    	ADD.D F4, F0, F2
    	; 源操作数寄存器 F4 依赖于前一条指令
    	S.D F4, 0(R1)
          
  • 名字相关性(Name Dependences) 名字相关性可以进一步细分为反相关性(Antidependence)和输出相关性(Output Dependence)。
  • 反相关性(Antidependence) 反相关性是指:指令i以某个寄存器作为源操作数时,后续指令j同时以该寄存器作为目的操作数。例如:
    S.D F4, 0(R1)
    	DADDUI R1, R1, #8
          
    在这种情况下,如果在不加处理的进行乱序执行时,一旦第二条指令先回写结果,第一条指令将得到错误的结果。
  • 输出相关性(Output Dependence)

    指令i和后续指令j以同一个寄存器作为目的操作数。例如:

    S.D R4, 0(R1)
    	AND R4, R2, R3	
          

    在这种情况下,如果第二条指令先于第一条指令结束,那么R4寄存器将得到错误的值。

    和真实数据相关性比较起来,在反相关性和输出相关性中,指令与指令直接并不存在数据的传递,因此又被统称为名字相关性,名字相关性并不是真正意义上的相关性。寄存器重命名(Register Renaming)是消除名字相关性的有效手段,寄存器重命名可以同编译器来实现,也可以通过CPU硬件实现。考虑下面的代码:

    DIV.D F0, F2, F4
    	ADD.D F6, F0, F8
    	S.D   F6, 0(R1)
    	SUB.D F8, F10, F14
    	MUL.D F6, F10, F8
          

    在这个例子中,ADD.D和SUB.D之间存在一个反相关性,ADD.D F6, F0, F8中,F8作为源操作数,而SUB.D F8, F10, F14中,F8作为目的操作数,如果SUB.D先执行完毕,则ADD.D得到错误的结果。

    ADD.D和MUL.D之间存在一个输出相关性ADD.D F6, F0, F8中,F6是目的操作数,MUL.D F6, F10, F8中,F6也是目的操作数,如何MUL.D先执行完毕,之后ADD.D再执行,那么F6寄存器中的值就是错误的。(注意ADD.D和S.D之间不存在输出相关性,S.D中F6是源操作数,他们之间存在着真实数据相关性的关系,我们暂且不讨论。)

    对于反相关性,如果SUB.D回写结果(到F8)时,ADD.D还没有执行完毕(使用F8作为源操作数),那么CPU会分配一个临时的寄存器T1,把SUB.D的结果保存到T1中,同样在执行后续指令时,把F8替换为T1。于是在执行阶段,上述指令变为:

    DIV.D F0, F2, F4
    	ADD.D F6, F0, F8    ; 滞后
    	S.D   F6, 0(R1)
    	SUB.D T1, F10, F14  ; 超前
    	MUL.D F6, F10, T1   ; 超前
          
    对于输出相关性,如果MUL.D回写结果时(到F6),ADD.D还没有回写结果到F6,那么CPU会为ADD.D分配一个临时的寄存器T2,于是代码执行时,上述指令变为:
    DIV.D F0, F2, F4
    	ADD.D T2, F0, F8    ; 滞后
    	S.D   T2, 0(R1)     ; 滞后
    	SUB.D T1, F10, F14  ; 超前
    	MUL.D F6, F10, T1   ; 超前
          
    由于以上各种相关性的存在,如果不加处理的进行乱序执行,会出现以下“数据冒险”(Data Hazards),在下面的讨论中,假设指令j是指令i的后续指令。
  • RAW (Read After Write)

    指令i以某个寄存器作为目的操作数,在指令i完成写入操作前,指令j读该寄存器进行读取操作。例如:

    ; 如果第二条指令先执行,F0中的值是错误的。
    	DIV.D F0, F2, F4
    	ADD.D F6, F0, F8
          
  • WAW(Write After Write)

    指令i和指令j同时以同一个寄存器作为目的操作数。例如:

    ; 如果第二条指令先执行,之后F0中的值是“旧”的。
    	DIV.D F0, F2, F4
    	ADD.D F0, F1, F8
          
  • WAR(Write After Read)

    指令i以某一个寄存器作为源操作数,后续指令j以同一寄存器作为目的操作数,在指令i读操作数之前,指令j完成对目的寄存器的写入操作。例如:

    ; 如果第二条指令先执行,第一条指令会出错。
    	DIV.D F0, F2, F4
    	ADD.D F4, F1, F8
          
  • 控制相关性(Control Dependences)

    控制相关性主要由条件转移引起的,例如:

    DADDU R2, R3, R4
    	BEQZ  R2, L1
    	LW    R1, 0(R2)
    L1:
    	......
          

    在这个例子中BEQZ的运行结果出来之前,LW不能执行。在乱序执行的CPU中,可以通过转移预测解决控制相关性。

    乱序执行可以分为按序发射-无序完成(In-Order Issue, Out-Of-Order Completion),无序发射-无序完成(Out-Of-Order Issue, Out-Of-Order Completion)。无论哪一种方式,都需要CPU在执行过程中,决策各条指令的执行时机,因此又被称为指令动态调度,或者动态动态派遣(Dynamic Scheduling)。下面我们先来看按序发射-无序完成(In-Order Issue, Out-Of-Order Completion)的具体例子。

  • 按序发射-无序完成(In-Order Issue, Out-Of-Order Completion) 首选我们来看看按序发射-无序完成带来的好处,假设加法操作占用2个时钟周期,乘法操作占用10个时钟周期,除法操作占用40个时钟周期。下列指令的流水操作情况:
    时钟周期
    指令 1 2 3 4 6 ... 13 14 15
    MUL.D F0, F2, F4 取指 译码 执行 回写
    SUB.D F8, F6, F2 取指 译码 执行 回写
    SUB.D F8, F6, F2 取指 译码 执行
    在没有数据相关性及资源冲突的情况下,无序完成可以使后续指令及早进入流水队列,从而提高流水线的效率。
  • 基于Scoreboard的动态指令调度

    Scoreboard算法能够在按序发射-无序完成的实现中,自动动检测和消除相关性。在这一实现中,指令按照源二进制程序的顺序发射到Scoreboard单元,Scoreboard有一个指令队列,保存译码后的指令,当Scoreboard检测到某条指令可以执行时,就立刻调度该指令,而忽略它们的原始顺序。在Scoreboard算法中,指令执行需要经过以下阶段:

  • 发射(Issue)

    指令译码之后,根据指令的操作确定该条指令所需要的功能部件,如果当前功能部件是空闲的(保证了没有资源冲突(Structural Hazards),并且当前处于执行阶段的指令中(可能存在多条指令乱序执行),没有任何一条指令的目的操作数(写入对象)与当前待发射指令的目的操作数相同(保证了不会导致WAW冒险(输出相关性)。),就把译码后的指令提交到Scoreboard指令队列,更新Scoreboard的相关数据结构。否则停止发射,在这种情况下,如果取指队列还没有满,取指工作会继续。

  • 读操作数(Read Operands)

    Scoreboard监视到指令队列中的每一个源操作数寄存器,如果没有任何一条处于运行阶段(多条指令)的指令对该寄存器进行写入,或者写入操作已经完成时,Scoredboard通知相关功能单元,可以为该条指令读取源操作数。在这个过程中,Scoreboard保证了当前读取到的源操作数是最新的,消除了RAW冒险的可能。

  • 执行(Execution)

    源操作数准备就绪之后,进入执行阶段,不同的功能部件的执行时间可能不一致,当执行完成后,会通知Scoreboard.

  • 回写(Write Result)

    当Scoreboard接到执行单元的执行完毕的通知之后,Scoreboard检测回写的目标寄存器当前某条指令的源寄存器一致,并且该条指令还没有完成读源操作数(消除WAR冒险)。例如:

    DIV.D F0,  F2, F4
    	ADD.D F10, F0, F8
    	SUB.D F8,  F8, F14
          

    在SUB.D回写结果之前,如果ADD.D还没有完成对操作数,则SUB.D必须等待,如果不存在WAR冒险,或者冒险已经消除,Scoreboard通知相关单元进行回写操作。

    为了完成以上检测,Scoreboard需要维护3张表格,分别是指令状态,功能部件状态,以及寄存器状态。其中指令状态表格如下所示:

    指令状态(Instruction Status)
    指令(Instruction) 发射(Issue) 读操作数(Read Operands) 执行完毕(Execution Complete) 回写(Write Result)
    L.D F6, 34(R2) Y Y Y Y
    L.D F2, 45(R3) Y Y Y
    MUL.D F0, F2, F4 Y
    SUB.D F8, F6, F2 Y
    DIV.D F10, F0, F6 Y
    ADD.D F6, F8, F2
    Scoreboard在这个表中记录已发射指令的状态。功能部件状态表如下所示:
    功能部件状态(Functional Unit Status)
    Name Busy Op Fi Fj Fk Qj Qk Rj Rk
    Integer Yes Load F2 R3 No
    Mult1 Yes Mult F0 F2 F4 Integer No Yes
    Mult2 No
    Add Yes Sub F8 F6 F2 Integer Yes No
    Divide Yes Div F10 F0 F6 Mult1 No Yes
    每一条已发射的指令都在这个表中有一条对应的记录。其中各个字段的意义如下:
  • Functional Unit Name 表示一个功能部件,例如Integer表示整数运算单元,Mult1表示第一个乘法器,Add表示加法器(注意SUB指令实际上使用加法器。),Divide表示除法器等等。
  • Busy 表示该功能部件是否空闲。
  • Fi 表示该条指令对应的目的操作寄存器。
  • Fj, Fk 表示指令对应的源操作寄存器。
  • Qj, Qk 当源操作数没有就绪时,Qj, Qk分别记录哪一个功能部件为该条指令计算Fj, Fk.
  • Rj, Rk 用于指示Fj, Fk已经准备就绪,但是Fj, Fk读源操作数还没有完成时,对应的Rj, Rk为Yes,如果Fj, Fk没有准备就绪,或者读操作数已经完成,则Rj, Rk为No,因此(在有两个源操作数的情况下)当Fj, Fk都为Yes时,可以进行读操作数。

    由于字段比较多,我们举两个例子来说明这个表格是如何填写的。例如在上面指令状态表中的MUL.D F0, F2, F4指令已经发射,在功能部件状态表中的第二项(和指令顺序没有关系)Name为Mult1表示当前有一条指令要使用乘法器1,同时Busy字段为Yes;Op字段为Mult,表示要进行乘法操作;Fi为F0,表示该条指令的目的操作数为F0;Fj, Fk分别为F2, F4表示两个源操作数;Qj为Integer表示当前这条指令的源操作数Fj从一个整数运算部件的输出获取(F2来自L.D F2, 45(R3),整数运算部件用于计算45+R3);Qk为空,表示另外一个源操作数F4已经就绪; Rj为No,表示第一个源操作数(F2)还没有准备就绪,Rk为Yes,表示第二个源操作数(F4)已经准备就绪,当Rj和Rk都为Yes后,可以进行读操作数。

    Scoreboard的第三个表是Register Result Status表:

    寄存器状态(Register Result Status)
    F0 F2 F4 F6 F8 F10 F12 ... F30
    Functional Unit Mult1 Integer Add Divide

    Register Result Status用于指示哪一个功能部件将要把结果回写到哪一个寄存器。例如上面的例子中,F0为Mult1,表示有一条指令以F0为目的操作寄存器,将来会写F0.(对应MUL.D F0, F2, F4).

    在上面这个例子中,由于没有空闲的功能单元,因此最后一条指令不能发射。由于第三条(MUL.D),第四条指令(SUB.D)的源操作寄存器(F2)是第二条指令(L.D)的目的操作寄存器(RAW),因此不能进行读操作数,同样第五条指令(DIV.D)的源操作寄存器(F0)是第三条指令(MUL.D)的目的操作数,因此不能读操作数。

    为了进一步说明Scoreboard的具体操作过程,假设加法操作占用2个时钟周期,乘法操作占用10个时钟周期,除法操作占用40个时钟周期。下一步假设L.D操作已经完成,因此Scoreboard的状态变成下面这个样子。

    第二条指令(L.D)回写已经完成,Integer部件变成空闲状态。第六条指令(ADD)因为没有空闲的加法器,依然不能发射,此时如果第七条指令使用Integer部件,也不能发射,所以被称为按序发射。

    第三(MUL.D),第四(SUB.D)条指令可以读操作数。因此Scoreboard的状态如下:

    指令状态(Instruction Status)
    指令(Instruction) 发射(Issue) 读操作数(Read Operands) 执行完毕(Execution Complete) 回写(Write Result)
    L.D F6, 34(R2) Y Y Y Y
    L.D F2, 45(R3) Y Y Y Y
    MUL.D F0, F2, F4 Y Y
    SUB.D F8, F6, F2 Y Y
    DIV.D F10, F0, F6 Y
    ADD.D F6, F8, F2
    功能部件状态(Functional Unit Status)
    Name Busy Op Fi Fj Fk Qj Qk Rj Rk
    Integer No
    Mult1 Yes Mult F0 F2 F4 No No
    Mult2 No
    Add Yes Sub F8 F6 F2 No No
    Divide Yes Div F10 F0 F6 Mult1 No Yes
    由于第二条指令的写入已经完成,所以寄存器状态表变为:
    寄存器状态(Register Result Status)
    F0 F2 F4 F6 F8 F10 F12 ... F30
    Functional Unit Mult1 Add Divide

    读操作数完成后,第二(MUL.D),第三(SUB.D)条指令同时开始执行,两个时钟周期后,第三条指令(SUB.D)执行完毕,Scoreboard开始做如下检查:

    回写的目标寄存器当前某条指令的源寄存器一致,并且该条指令还没有完成读源操作数(消除WAR冒险)

    这个条件并不满足,因此回写可以顺利进行,假设回写占用1个时钟周期,此时第二条指令(MUL.D)没有结束。这时加法器已经空闲,因此第六条指令(ADD.D)可发射,发射后,由于没有任何一个指令会对该指令的两个源操作数(F8, F2)进行写人,因此第六条指令可以立即完成读源操作数的步骤。Scoreboard状态如下:

    指令状态(Instruction Status)
    指令(Instruction) 发射(Issue) 读操作数(Read Operands) 执行完毕(Execution Complete) 回写(Write Result)
    L.D F6, 34(R2) Y Y Y Y
    L.D F2, 45(R3) Y Y Y Y
    MUL.D F0, F2, F4 Y Y
    SUB.D F8, F6, F2 Y Y Y Y
    DIV.D F10, F0, F6 Y
    ADD.D F6, F8, F2 Y Y
    功能部件状态(Functional Unit Status)
    Name Busy Op Fi Fj Fk Qj Qk Rj Rk
    Integer No
    Mult1 Yes Mult F0 F2 F4 No No
    Mult2 No
    Add Yes ADD F6 F8 F2 No No
    Divide Yes Div F10 F0 F6 Mult1 No Yes
    寄存器状态(Register Result Status)
    F0 F2 F4 F6 F8 F10 F12 ... F30
    Functional Unit Mult1 F6 Divide
    从上面我们可以看出,第4条指令(SUB.D)在第三条指令之前完成,因此称为无序完成。当第三条指令(MUL.D)完成时,此前第六条指令(ADD.D)也已经完成,但是因为其目的操作数为F6,而第五条指令(DIV.D)还没有读源操作数,因此第六条指令(ADD.D)不能进行回写操作。Scoreboard的状态如下:
    指令状态(Instruction Status)
    指令(Instruction) 发射(Issue) 读操作数(Read Operands) 执行完毕(Execution Complete) 回写(Write Result)
    L.D F6, 34(R2) Y Y Y Y
    L.D F2, 45(R3) Y Y Y Y
    MUL.D F0, F2, F4 Y Y Y
    SUB.D F8, F6, F2 Y Y Y Y
    DIV.D F10, F0, F6 Y
    ADD.D F6, F8, F2 Y Y Y
    功能部件状态(Functional Unit Status)
    Name Busy Op Fi Fj Fk Qj Qk Rj Rk
    Integer No
    Mult1 Yes Mult F0 F2 F4 No No
    Mult2 No
    Add Yes ADD F6 F8 F2 No No
    Divide Yes Div F10 F0 Add Mult1 No Yes
    寄存器状态(Register Result Status)
    F0 F2 F4 F6 F8 F10 F12 ... F30
    Functional Unit Mult1 Add Divide
    通过以上讨论,我们可以看到, Scoreboard存在以下缺点:
  • 按序发射可能会导致某个功能部件“长期”空闲,例如在上面的例子中,当Integer部件空闲时,由于资源冲突第六条指令(ADD.D)不能发射,如果后续指令正好是一条要使用Integer部件的指令,就可以通过提前发射该条指令来进一步提高性能。为此提出了无序发射-无序完成(Out-Of-Order Issue, Out-Of-Order Completion)的方法。无序发射-无序完成为译码单元设置专门的缓冲器,从而形成一个译码后的指令队列,通过随时检测该队列,一旦某条指令所需的功能部件空闲就立即发射。假设上面的例子中,最后一条指令(ADD.D)之后是一条需要使用Integer部件的指令,则该指令可在ADD.D之前发射。
  • 对于所有的数据冒险(Data Hazards),RAW, WAR, WAW都一律通过等待的方式来避免,例如在当第六条指令ADD.D F6, F8, F2执行结束时,而第五条指令DIV.D F10, F0, F6由于等待F0的结果,因此还没有读源操作数,为了消除这个WAR冒险(F6),因此ADD.D回写(F6)操作被推迟,从而导致加法器不能及时的变成空闲状态。为此提出了寄存器重命名的方法。通过寄存器重命名(Register Renaming)可以在避免额外等待的同时消除WAR和WAW的数据冒险。
  • Tomasulo算法

    Tomasulo算法是另外一个按序发射-无序完成的实现,它采用寄存器重命名(Register Renaming)来消除WAR和WAW类型的数据冒险。Tomasulo算法和Scoreboard非常类似,它也有3个表,分别是指令状态(Instruction Status)表,保留栈(Reservation Stations),和寄存器状态(Register Status),其中保留栈的作用和Scoreboard中的功能部件状态(Functional Unit Status)类似。各个表的内容如下所示:

    指令状态(Instruction Status)
    指令(Instruction) 发射(Issue) 执行完毕(Execut) 回写(Write Result)
    L.D F6, 34(R2) Y Y Y
    L.D F2, 45(R3) Y Y Y
    MUL.D F0, F2, F4 Y Y
    SUB.D F8, F6, F2 Y Y Y
    DIV.D F10, F0, F6 Y
    ADD.D F6, F8, F2 Y Y Y
    和Scoreboard相比较,Tomasulo的指令状态表省去了读操作数的状态。在这里我们看到第五条指令(DIV.D)之前,第六条指令(ADD.D)已经完成了回写操作,由于采用了寄存器重命名,消除他们之间的WAR冒险(R6),所以ADD.D可以提前进行回写操作。
    保留栈(Reservation Stations)
    Name Busy Op Vj Vk Qj Qk A
    Load1 No
    Load2 No
    Add1 No
    Add2 No
    Add3 No
    Mult1 Yes Mult Mem[45+Regs[R3]] Regs[F4]
    Mult2 Yes DIV Mem[34+Regs[R2]] Mult1
    保留栈中,各个字段的意义如下:
  • Name 所需的功能部件。
  • 指示保留栈和对应的功能部件是否空闲。
  • Op 指令中的具体操作。
  • Qj, Qk 保留栈中,哪一条指令将为该条指令产生源操作数。当Qj, Qk为0时,表示源操作数已经准备好,保存在Vj, Vk中,或者不需要某个源操作数。
  • Vj, Vk 操作数的实际值,对于每一个操作数,当Qj, Qk非0时,表示操作数还没准备好,此时Vj, Vk为0,同时Qj, Qk指向产生该操作数的指令。对于Load指令,Vj保存基址寄存器的值,Vk保存偏移量。
  • A 用来为Load/Store指令保存地址信息,初始化时,保存立即数(偏移量),当地址计算出来后,保存基址+偏移的计算结果。

    这个表是实现寄存器重命名的关键,在上面的例子中,DIV.D F10, F0, F6这条指令的源操作数,F6已经准备好,所以Vk保存F6的实际值(这里记做Mem[34+Regs[R2]]。),但是F0还没有准备好,还在等待MUL.D F0, F2, F4回写结果,因此Qj为Mult1,表示Vj这个源操作数将由乘法器Mult1产生,当Mult1回写结果时,把这个结果在公共数据总线(Common Data Bus)上广播,每一个部件都能看到这个结果,保留栈中每一条等待该结果的指令会更新这个结果。

    从上面的分析中可以看出,在DIV.D F10, F0, F6还没有执行时,ADD.D F6, F8, F2就可以把计算结果回写到F6中。

    寄存器状态(Register Result Status)
    F0 F2 F4 F6 F8 F10 F12 ... F30
    Field Mult1 Mult2
    现在,再来看在Tomasulo算法中,指令的执行过程就简单的多了(略去了取指令和指令译码的步骤。):
  • 发射(Issue) 获取下一条指令,如果保留栈(Reservation Station)中有匹配的空闲项目(消除了 Structural Hazard),就把改指令提交到保留栈中,并且更新相关数据结构。如果源操作数已经就绪,就把它读到保留栈的Vj, Vk中,如果还没有就绪,就在Qj, Qk中记录将产生源操作数的部件。这个步骤被称为寄存器重命名(Register Renaming),消除了WAR和WAW.
  • 执行(Execute) 如果源操作数没有就绪,就监视通用数据总线(Common Data Bus),,将来如果某个部件完成运算时,会把结果广播到通用数据总线(Common Data Bus)上,这时可以更新Vj, Vk,并开始执行。通过等待源操作数就绪后才开始执行,消除了RAW类型的数据冒险。
  • 回写(Write Result) 计算完成时,把通用数据总线上的结果回写到目标寄存器,以及相应的保留栈中。和Scoreboard比较起来,通过寄存器重命名来消除了WAR和WAR类型的数据冒险,因此回写操作无需等待。

    最后我们来看看超级流水线和超标量的区别。

  • 超级流水线超级流水线是对普通流水线的进一步细分,例如把取指令分为两个或者更多的流水步骤,Pentium4的流水级数超过20级。
  • 超标量在乱序执行的基础上,超标量处理器拥有多条相互独立的流水线,它们同时取指令,乱序执行。例如,我们在龙芯2F的参数中看到的4发射,这里的4发射是指有4条流水线,一个周期能够发射4条指令。

来源:http://www.osplay.org/modules/article/view.article.php/15/c9

继续阅读