这是www.agner.org/optimize文章系列中The microarchitecture of Intel, AMD and VIA CPUs这篇文章中的第六部分,主要介绍Ppro PII and PIII的流水线。Agner对于各体系结构的描述并不是按照cpu出现的前后时间来进行的。但是我个人认为按照时间前后顺序来描述更有助于理解新事物出现的前因后果以及发展顺序。所以把这一章提前。不多说了,进入正文。


6.1 PPro, P2 and P3的流水线

         1995年的Pentium Pro是英特尔第一个引入乱序执行的处理器。 这款微体系结构设计非常成功。 从PPro开始到后来的很多代处理器,直到今天我们常见的很多处理器都是该设计的进一步开发或者技术借鉴。但是后继的很多型号中,需要绕过不太成功的Pentium 4或Netburst架构。


    英特尔在各种手册和教程中对PPro,P2和P3微处理器的流水线进行了说明,不幸的是,这些手册和教程不太容易找齐。因此,我将在这里描述下PPro,P2和P3的流水线。

the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP
上图是Pentium Pro pipeline结构图,各阶段描述如下:
BTB0,1:分支预测,主要负责从哪取下一条指令。
IFU0,1,2:取指单元。
ID0,1:指令译码器。
RAT:寄存器别名表,负责寄存器重命名。
ROB Rd:重排序缓存读
RS:保留站
Port0,1,2,3,4:连接执行单元的发射口
ROB wb:把执行结果写回重拍序缓存
RRF:寄存器注销文件

         流水线中的每个阶段至少需要一个时钟周期。分支预测在后面单独进行解释。 流水线中的其他阶段将下面逐一说明。


6.2 取指

         指令代码以16字节对齐的块从代码高速缓存中提取,存放到可以容纳两个16字节块的双缓冲区中。简单来说就是缓冲区有两个16字节大小的空间,每次取指是16字节对齐的块。双缓冲区的目的是为了译码跨16字节边界(如可被16整除的地址)的指令。 另外还有一个前提,那就是intel最长指令貌似是15字节长度。代码以块的形式从双缓冲区传递到译码器,这种块称之为IFETCH块(指令提取块)。IFETCH块最长为16个字节。 在大多数情况下,取指单元提取每个IFETCH块从指令边界而不是16字节边界开始。 然而,取指单元需要来自指令长度解码器的信息,以便知道指令边界在哪里。 如果无法及时获得此信息,取指单元就提取一个16字节边界的IFETCH块。 这种复杂机制将在下面更详细地讨论。
        如果要及时处理jump指令前后的取指,双缓冲区还是不够大。 如果IFETCH块包含跨16字节边界的jump指令,则双缓冲区需要保留两个连续对齐的16字节代码块才能完整生成jump指令。如果jump后的第一条指令跨16字节边界,则双缓冲区需要重新加载两个新的16字节代码块,然后才能生成有效的IFETCH块。 这里描述的最坏情况我个人很难理解,只能以个人猜测,可能是下面的情况:
the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP

    我的这个例子可能不合理,按照个人理解用来说明意思应该差不多吧。 jnz跳转指跨16字节边界(1020h),所以在double buffer中是占用两个16字节的buffer。跳转到ll标志处,也跨16字节边界(1010h),要生成完整的mov [esi], eax,也需要double buffer的两个buffer来拼。这是相对于double buffer来说的,不管IFETCH从哪里开始,取多长,和double buffer没有直接关系,当然也有一些限制,起码应该在double buffer范围内。也就是说double buffer的32字节范围内,IFETCH块可以从16字节边界开始,也可以从指令边界开始。如跳转到ll标志处的IFETCH块会根据jump不同的状态可能从1000h处开始取IFETCH块,也可能从1010h处取。具体看下面agner总结的表。
以上这段是自己的理解,不知道是自己理解有误,还是其它情况。有了解的同学帮忙修改或改进下面继续。
    这意味着,在最坏的情况下,jump后的第一条指令的解码可能会延迟两个时钟周期。包含jump指令的IFETCH块中的16字节边界有一个时钟损失,jump后面的第一条指令中16字节起始有一个时钟损失。如果译码一个IFETCH块需要一个以上的时钟周期,则可以使用这一额外时间进行预取。 这可以补偿jump前后16字节边界处的时钟损失。
具体的延时情况如下table6.1:the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP
     第一列是指译码一个指令提取块里的所有指令需要的时间。这里有一个概念--decode group
A group of up to three instructions that are decoded in the same clock cycle is called a decode group,即一个时钟周期同时译码的几条指令称为一个decode group,一个组最多同时译码3条指令。也就是说一个IFETCH有几个组,就需要几个时钟周期。关于这个表,有必要说一说,我按照自己的理解来梳理,有错误请指出,我会及时改进。
第一列是包含jump的IFETCH的译码组数量,也就是译码一个IFETCH块需要的时钟周期;
第二列是包括jump的IFETCH块是否跨16字节边界,0表示不跨16字节边界,1表示跨16字节边界;
第三列是jump后第一条指令是否跨16字节边界,0和1的表示与第二列相同;
第四列是译码的延时;
第五列是jump后的第一个IFETCH是从16字节边界开始还是从jump后的第一条指令开始,by 16表示从16字节边界取IFETCH块,to instruction表示从指令处开始取IFETCH块;

第一行:
the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP
表示,当前含有jump的IFETCH译码需要一个时钟周期,当前IFETCH块,以及jump后的第一条指令都不跨16字节边界,译码没有延时,jump后的IFETCH从16字节边界提取,即jump跳转后的第一个IFETCH块从下面红色的地址开始提取,尽管有效指令在1015h处。
the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP

第二行:
the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP
表示,当前含有jump的IFETCH块译码需要一个时钟周期,当前IFETCH块不跨16字节边界,jump后的第一条指令跨16字节边界,译码延时1个时钟周期,jump后的IFETCH从指令边界提取,即jump跳转后的第一个IFETCH块从上图中1015h处取址。

第三行:
the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP
表示,当前含有jump的IFETCH译码需要一个时钟周期,当前IFETCH块跨16字节边界,jump后的第一条指令不跨16字节边界,译码延时1个时钟周期,jump后的IFETCH从16字节边界提取,即jump跳转后的第一个IFETCH块从上面图中红色的地址开始提取,尽管有效指令在1015h处。
下面的所有行,除了第一列需要的时钟周期数不一样,其它都一样。从这里可以看得出来,译码需要的时钟越多,留给后面取址的时间越多,取址就会从指令起始提取。其它就不多说了。

    如果一条指令超出了IFETCH块的末尾,则它将进入下一个IFETCH块,指令从该块的第一个字节开始。 因此,指令提取单元需要知道每个IFETCH块中的最后一条完整指令在何处结束才能生成下一个IFETCH块。 该信息由指令长度解码器生成,该指令长度解码器位于流水线中的阶段IFU2中(图6.1)。 指令长度解码器每个时钟周期可以确定三条指令的长度。 例如,如果一个IFETCH块包含10条指令,第10条跨16字节边界,则将需要三个时钟周期,才能知道IFETCH块中的最后一条完整指令在何处结束并且确定下一个IFETCH块的起始。上面红色描述的意思是:
the microarchitecture of intel,amd and via cpus(五)PPro,II and III pipeline-LMLPHP
假设第一个IFETCH从1000h开始,到1010h结束,但是1007h处的mov [mem],0指令跨1010h,这样的话,第二个IFETCH将从mov [mem],0指令所处的1007h处开始,并且执行到第二个IFETCH的时候会从最开始执行。

6.3 指令译码

6.3.1 指令长度译码

    IFETCH块先进入指令长度译码器,确定每条指令的开始和结束位置。这在流水线中是非常关键的一个阶段,因为它限制了可以实现的并行度。我们希望每个时钟周期获取多条指令,每个时钟周期译码多条指令,并且每个时钟周期执行多于一个μop,以提高速度。但是,当指令具有不同的长度时,很难并行译码指令。您需要先译码第一条指令,才能知道第二条指令的长度和开始位置,然后才能开始译码第二条指令。因此,一个简单的指令长度译码器每个时钟周期只能处理一条指令。 但是PPro微体系结构中的指令长度译码器每个时钟周期可以确定三个指令的长度,甚至可以将此信息足够早的反馈到指令提取单元,以便生成新的IFETCH块,以供指令长度译码器在下一个时钟周期正常工作。这是一个了不起的成就,由于一个IFETCH块最大16字节,我认为是通过对所有16个可能的起始地址并行译码来实现的。

6.3.2 4-1-1规则

    在指令长度解码器之后,指令进入译码器,译码器将指令翻译为μop。 PPro、II和III共有三个译码器,这三个解码器称为D0,D1和D2,它们可以并行工作,因此每个时钟周期最多可以译码三条指令。 在同一时钟周期内译码的一组最多三条指令称为译码组。D0可以处理所有指令,并且每个时钟周期最多可产生4μop。 D1和D2仅能处理简单的指令,每个指令最多产生一个μop,并且长度不超过8个字节。 IFETCH块中的第一条指令始终只能由D0译码。 如果可能,接下来的两条指令将分配到D1和D2。 如果要进入D1或D2的指令由于它们产生一个以上的μop或因为指令的长度超过8个字节而不能被D1和D2处理,则必须等待,直到D0空出,然后在D0上处理。随后的指令也都会向后延迟。如下面例子:

点击(此处)折叠或打开

  1. ; Example 6.1a. Instruction decoding
  2. mov [esi], eax   ; 2 uops, D0
  3. add ebx, [edi]   ; 2 uops, D0
  4. sub eax, 1       ; 1 uop, D1
  5. cmp ebx, ecx     ; 1 uop, D2
  6. je  L1           ; 1 uop, D0
    在此例中,第一条指令分配到译码器D0。 第二条指令不能分配给D1,因为它产生一个以上的μop。 因此,它将延迟到下一个时钟周期,当D0再次准备就绪时,分配给D0。 第三条指令按序分配D1,第四个指令按序分配给D2。这样第二三四条指令可以并行译码,用一个时钟周期, 最后一条指令分配给D0。 所以整个序列需要三个时钟周期才能完全译码。可以通过交换第二和第三条指令来改进上面的例子:

点击(此处)折叠或打开

  1. ; Example 6.1b. Instructions reordered for improved decoding
  2. mov [esi], eax   ; 2 uops, D0
  3. sub eax, 1       ; 1 uop,  D1
  4. add ebx, [edi]   ; 2 uops, D0
  5. cmp ebx, ecx     ; 1 uop,  D1
  6. je  L1           ; 1 uop,  D2
这样的话,第一二两条指令使用一个时钟周期,剩下的三条指令使用另外一个时钟周期。由于在译码器之间更好地分配了指令,因此译码仅需要两个时钟周期。
     
    当按照4-1-1模式排序指令时,可获得最大的译码速度:如果每三条指令第一条产生4μop,而接下来的两条指令各自产生1μop,则译码器每个时钟周期可以产生6μop。 指令排序2-2-2模式将获得最小译码速度,每个时钟产生2μops,因为所有2μop指令都进入了D0。 建议您按照4-1-1规则对指令进行排序,以便每条产生2、3或4μop的指令后面紧跟两条分别产生1μop的指令。 产生超过4μops的指令必须进入D0,它需要两个或更多时钟周期才能译码,并且其它任何指令都不能与它并行译码。

6.3.3 IFETCH块边界

    更复杂的是,IFETCH块中的第一条指令始终进入D0。如果代码是根据4-1-1规则进行调度的,并且原本打算用于D1或D2的1-μop指令之一恰好位于IFETCH开始处中,则该指令进入D0,这样4-1-1模式损坏。这会将译码延迟一个时钟周期。指令获取单元无法将IFETCH边界调整为4-1-1模式,因为我认为,关于哪条指令产生的指令超过1μop的信息仅在管道的下两级才可能知道。
    由于很难猜测IFETCH边界在哪里,因此很难处理该问题。解决此问题的最佳方法是调度代码,以使译码器每个时钟周期可以生成3μop以上的内容。流水线中的RAT和RRF级(图6.1)每个时钟周期最多只能处理3μop。如果按照4-1-1规则对指令进行排序,以便我们可以预期每个时钟周期至少4μops,那么即使在每个IFETCH边界损失一个时钟周期,最后仍然保持平均译码器吞吐量不低于每个时钟3μop,这勉为其难也是可以接受的
    另一种解决方法是使指令尽可能短,以便将更多指令放入每个IFETCH块。 每个IFETCH块更多的指令意味着更少的IFETCH边界,因此4-1-1模式的连续性会更好。 例如,您可以使用指针而不是绝对地址来减小代码大小。 有关如何减小指令大小的更多建议,请参见手册2:“使用汇编语言优化子例程”。

     在某些情况下,可以对代码进行操作,以使预计用于译码器D0的指令落在IFETCH边界。但是通常很难确定IFECTH边界在哪里,并且可能并不值得。首先,您需要使代码段对齐,以便知道16字节边界在哪里。然后,您必须知道要优化的代码的第一个IFETCH块从哪里开始。查看汇编器的输出列表,以查看每条指令的时间。如果您知道一个IFETCH块从哪里开始,那么您可以通过以下方式找到下一个IFETCH块从哪里开始:使IFETCH块长16个字节。如果它在指令边界处结束,那么下一个块将从此处开始;如果它以一条未完成的指令结尾,那么下一个块将从该指令的开头开始。这里只计算指令的长度,它们生成多少μop或做什么都无所谓。这样,您就可以通过代码来完成所有工作,并标记每个IFETCH块的开始位置。其中最大的问题是要知道从哪里开始。以下是一些准则:

    ? 根据表6.1,跳转,调用或返回之后的第一个IFETCH块可以从第一条指令或最前面最近的一个16字节边界开始。如果将第一条指令对齐以16字节为边界开始,则可以确保第一个IFETCH块从此处开始。为此,您可能需要将重要的子程序条目和循环条目对齐16字节边界。

    ? 如果两个连续指令的总长度超过16个字节,则可以确定第二个指令与第一个指令不适合在同一IFETCH块中,因此,您始终会得到一个从第二条指令开始的IFETCH块。您可以以此为起点来查找后续IFETCH块的起始位置。

    ? 分支预测错误之后的第一个IFETCH块始于16字节边界。因此,在循环中,预测错误的循环之后的第一个IFETCH块将从最近的前一个16字节边界开始。
下面我从一个例子来表述:

点击(此处)折叠或打开

  1. ; Example 6.2. Instruction fetch blocks
  2. address     instruction     length   uops expected decoder
  3. ---------------------------------------------------------------------
  4. 1000h     mov ecx, 1000       5       1       D0
  5. 1005h LL: mov [esi], eax      2       2       D0
  6. 1007h     mov [mem], 0       10       2       D0
  7. 1011h     lea ebx, [eax+200]  6       1       D1
  8. 1017h     mov byte [esi], 0   3       2       D0
  9. 101Ah     bsr edx, eax        3       2       D0
  10. 101Dh     mov byte [esi+1],0  4       2       D0
  11. 1021h     dec edx             1       1       D1
  12. 1022h     jnz LL              2       1       D2
    假设第一个IFETCH块开始于地址0x1000,结束于0x1010。在MOV [MEM],0指令之前结束,也就是停在指令中间,因此下一个IFETCH块将从这条指令,也就是0x1007开始,到0x1017结束。这是在指令边界,因此第三个IFETCH块将从1017h开始,并覆盖循环的其余部分。译码所需的时钟周期数就是D0指令的数目,LL循环的每次迭代为5个D0,所以需要5个时钟周期。最后一个IFETCH块包含三个译码组,覆盖最后五个指令,并且它具有一个16字节边界(0x1020)。查看上面的表6.1,我们发现跳转后的第一个IFETCH块将从跳转后的第一条指令开始,即LL标签位于0x1005,结束于0x1015。在LEA指令之前结束,也就是说停在LEA指令之间,因此下一个IFETCH块将从0x1011到0x1021,最后一个从0x1021开始覆盖其余部分。现在,LEA指令和DEC指令都位于IFETCH块的开头,这迫使它们进入D0。现在,我们在D0中有7条指令,也就是第二次循环需要7个时钟进行译码。最后一个IFETCH块仅包含一个译码组(DEC ECX / JNZ LL),并且没有16字节边界。根据表6.1,跳转后的下一个IFETCH块将从16字节边界开始,即0x1000,这与我们在第一次迭代中的情况相同,这样循环交替使用5和7时钟周期进行译码。由于没有其他瓶颈,因此运行1000次完整循环迭代,将需要500x5+500x7=6000个时钟周期。如果起始地址不同,则循环的第一条或最后一条指令的边界为16个字节,那么它将花费8000个时钟。如果您有兴趣,可以对循环重新排序,以使D1或D2指令不落在IFETCH块的开头,那么您大概可以5000个时钟就可以完成1000次迭代。

    上面的示例是有意构造的,因此获取和译码是唯一的瓶颈。 可以提高译码效果的一件事是更改代码的起始地址,以避开不需要的16字节边界。记得要使代码段段落对齐,以便您知道边界在哪里。 如手册2:“使用汇编语言优化子例程”中的“使指令更长,以便对齐”中所述,可以操纵指令长度以便将IFETCH边界放置在所需的位置。

6.3.4 指令前缀


6.4 寄存器重命名

6.5 ROB读

6.6 乱序执行

6.7 退出

6.8 部分寄存器操作停顿

6.9 存储转发停顿

6.10 PPro,P2,P3的瓶颈







11-12 22:24
查看更多