Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1705806
  • 博文数量: 98
  • 博客积分: 667
  • 博客等级: 上士
  • 技术积分: 1631
  • 用 户 组: 普通用户
  • 注册时间: 2009-04-27 15:59
个人简介

一沙一世界 一树一菩提

文章分类

全部博文(98)

文章存档

2021年(8)

2020年(16)

2019年(8)

2017年(1)

2016年(11)

2015年(17)

2014年(9)

2013年(4)

2012年(19)

2011年(1)

2009年(4)

分类: 高性能计算

2020-11-12 09:04:25

    这是文章系列中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的流水线。


上图是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块。 这里描述的最坏情况我个人很难理解,只能以个人猜测,可能是下面的情况:

    我的这个例子可能不合理,按照个人理解用来说明意思应该差不多吧。 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:
     第一列是指译码一个指令提取块里的所有指令需要的时间。这里有一个概念--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块;

第一行:

表示,当前含有jump的IFETCH译码需要一个时钟周期,当前IFETCH块,以及jump后的第一条指令都不跨16字节边界,译码没有延时,jump后的IFETCH从16字节边界提取,即jump跳转后的第一个IFETCH块从下面红色的地址开始提取,尽管有效指令在1015h处。


第二行:

表示,当前含有jump的IFETCH块译码需要一个时钟周期,当前IFETCH块不跨16字节边界,jump后的第一条指令跨16字节边界,译码延时1个时钟周期,jump后的IFETCH从指令边界提取,即jump跳转后的第一个IFETCH块从上图中1015h处取址。

第三行:

表示,当前含有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块的起始。上面红色描述的意思是:

假设第一个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 指令前缀

        intel的指令每种前缀是一个字节,最多可以有4个前缀,当然也可以没有前缀。指令前缀也可能在译码器中产生时钟成本。 指令可以具有几种前缀,如手册2:“使用汇编语言优化子例程”中列出。
    1.如果指令中的立即数为16或32位,则立即操作数大小前缀会带来几个时钟的损失,因为立即操作数的长度会因前缀而改变。 示例(32位模式):
  

点击(此处)折叠或打开

  1. ; Example 6.3a. Decoding instructions with operand size prefix
  2. add bx, 9        ; No penalty because immediate operand is 8 bits signed
  3. add bx, 200      ; Penalty for 16 bit immediate. Change to ADD EBX, 200
  4. mov word [mem],9 ; Penalty because operand is 16 bits
也就是说32位模式下,立即操作数是16位,则会带来指令操作数大小前缀。另外如果是16位模式,你要操作数是32位,也会带前缀的。当然8位是个例外,不论16还是32位模式,8位操作数都不会生成带前缀指令。前缀最常见的就是0x66和0x67。这表明我们平时编程时,数据类型使用默认数据类型或者8位类型,会减少很多不必要的前缀带来的时钟代价。最后一条指令可以改进为下列模式:

点击(此处)折叠或打开

  1. ; Example 6.3b. Decoding instructions with operand size prefix
  2. mov eax, 9
  3. mov word [mem16], ax  ; No penalty because no immediate
    2.只要存在显式的内存操作数(即使没有位移),地址大小前缀都会受到影响,因为指令代码中r / m位的解释会被前缀更改。 仅包含隐式内存操作数的指令(例如字符串指令)对地址大小前缀没有任何时钟代价。

    3.段前缀在译码器中不造成任何时钟代价。

    4.重复前缀和锁定前缀在译码器中不会造成任何时钟代价。

    5.如果一条指令的前缀不止一个,则始终会受到惩罚。 每个额外的前缀通常要花费一个时钟。

6.4 寄存器重命名

    寄存器重命名由图6.1所示的寄存器别名表(RAT)控制。 自译码器出来的的μop通过队列进入RAT,然后到达ROB和预留站。 RAT每个时钟周期可处理3μop。 这意味着微处理器的总体吞吐量平均每个时钟周期永远不会超过3μops。

    微处理器有很多临时寄存器可使用,重命名的数量没有实际限制。 RAT可以在每个时钟周期重命名三个寄存器,甚至可以在一个时钟周期内重命名同一寄存器3次。

    此阶段还计算IP(instruction pointer)相对分支并将其发送到BTB0阶段。

6.5 ROB读

    RAT之后,就来到ROB读取阶段,如果已重命名的寄存器的值可用,则将这些值存储在ROB条目中。每个ROB条目最多可以具有两个输入寄存器和两个输出寄存器。输入寄存器的值有三种可能性:

    1    该寄存器最近未修改。 ROB读取阶段从永久寄存器文件中读取值,并将其存储在ROB条目中。上面着色的“寄存器”这里应该表示的是永久寄存器,即 EAX,EBX之类的寄存器。

    2    该值最近已修改。新值是已执行但尚未退出的μop的输出。我假设ROB读取阶段将从尚未退休的ROB条目中读取值并将其存储在新的ROB条目中。

    3    该值尚未准备好。所需的值是已排队但尚未执行的μop的即将的输出。新值目前还无法写入,但是一旦准备好,执行单元将立即将其写入新的ROB条目。

    情况1似乎是问题最少的情况。但是非常令人惊讶的是,这是唯一可能导致ROB读取阶段延迟的情况。原因是永久寄存器文件只有两个读取端口。 ROB读取阶段可以在一个时钟周期内从RAT接收多达三个μop,每个μop可以具有两个输入寄存器。这总共提供了多达六个输入寄存器。如果这六个寄存器不同并且全部存储在永久寄存器文件中,则通过寄存器文件的两个端口执行六次读取,一共需要三个时钟周期。这样之前的RAT阶段将暂停,直到ROB读再次准备就绪。如果译码器和RAT之间的队列已满,则译码器和指令提取也将停止。该队列只有大约十个条目,因此很快就会被填满。

    永久性寄存器读取的限制适用于指令使用的所有寄存器,但指令仅写入的寄存器除外。如下例:

点击(此处)折叠或打开

  1. ; Example 6.4a. Register read stall
  2. mov [edi + esi], eax
  3. mov ebx, [esp + ebp
    第一条指令产生两个μop:一个读取EAX,一个读取EDI和ESI。 第二条指令产生一个μop,读取ESP和EBP。 EBX不算作读取,因为它仅由指令写入。 假设这三个μop一起通过RAT。 我把这一起通过RAT的三个连续μop的组称为“三元组”。 由于ROB每个时钟周期只能处理两次永久性寄存器读取,而我们需要进行五次寄存器读取,因此三元组将在到达保留站(RS)之前一共需要3个时钟周期。 如果在三元组中读取3或4个寄存器,则将需要2个时钟周期。 同一寄存器可以在同一三元组中读取多次,而不增加时钟计数。上面的例子可以修改为如下的形势:

点击(此处)折叠或打开

  1. ; Example 6.4b. No register read stall
  2. mov [edi + esi], edi
  3. mov ebx, [edi + edi]
那么我们将只需要两个寄存器读取(EDI和ESI),这样三元组只需要一个时钟,不会产生多余延迟。

   情况2和3不会导致读寄存器停顿。 如果尚未通过ROB写回阶段,则ROB可以没有停顿的读取寄存器。 从RAT到ROB写回至少需要三个时钟周期,因此您可以确定至少在接下来的三个三元组中可以无延迟地读取一个先写入三元组中的寄存器。 如果通过重新排序,慢速指令,依赖链,高速缓存未命中或任何其他类型的停顿来延迟写回,则可以在不进一步延迟指令流的情况下读取寄存器。基本上就是说,如果但前uop需要的值在上一条指令中,不管当前时刻上一条指令是在执行但是没有结束,还是没有执行,都不会造成多余延迟。只要上一条指令的值可用,不必到写回阶段,当前指令就会第一时间拿到该值。一旦需要读的值进入写回阶段后,读寄存器就会产生延时了。更简单的说就是写后读不会造成延时。在硬件上这个机制好象就是“bypass”(旁路)

点击(此处)折叠或打开

  1. ; Example 6.5. Register read stall
  2. mov eax, ebx
  3. sub ecx, eax
  4. inc ebx
  5. mov edx, [eax]
  6. add esi, ebx
  7. add edi, ecx
    这6条指令每条都产生1 uop。 假设前三个uops一起通过RAT。 这3 uops读取寄存器EBX,ECX和EAX。 但是由于我们在读取EAX(第3行)之前就已经对其进行写操作(第1行),所以此次读取是没有延时的,并且不会停顿。 接下来的三个微指令读取EAX,ESI,EBX,EDI和ECX。 由于EAX,EBX和ECX均已在前面的三元组中进行了修改,并且尚未回写,因此读取它们没有延时,因此只有ESI和EDI有效计数,第二个三元组中也没有停顿。 如果第一个三元组中的SUB ECX,EAX指令更改为CMP ECX,EAX,则不会写入ECX,我们在第二个三元组中读取ESI,EDI和ECX,将会出现一个停顿状态。同样,如果将第一个三元组中的INC EBX指令更改为NOP或其他内容,那么我们将在第二个三元组中读ESI,EBX和EDI,出现停顿

   要计算读寄存器的次数,你必须包括该指令读取的所有寄存器。 这包括整数寄存器,标志寄存器,堆栈指针,浮点寄存器和MMX寄存器。XMM寄存器算2个寄存器,除非仅使用了一部分,例如: 在ADDSS和MOVHLPS中使用的情况。 段寄存器和指令指针不能算在内。 例如,
    SETZ AL中,标志寄存器算读,但AL不算。
    ADD EBX,ECX中,EBX和ECX都是读,但标志不算在内,因为它们仅被写入。
    PUSH EAX中,读EAX和堆栈指针算在内,然后写入堆栈指针。
这里要区分XMM和MMX两类寄存器,MMX首次出现在486上,是intel为了多媒体的加速执行,主要用于整形SIMD。到P3就引入SSE,这是针对SIMD扩展,也就出现了XMM寄存器了。再后来各种扩展依次出现。以致于不专门注意,你都感觉不到MMX的存在。现在用的多的是XMM,甚至还有YMM、ZMM,都是位数更宽的寄存器扩展。

   FXCH指令是一种特殊情况。它通过重命名来工作,但是不读取任何值,这条指令并不实质性的交换寄存器的值,只是改名字。因此不受受限于存器读取停顿的规则。FXCH指令生成一个μop,它不会根据寄存器读取停顿的规则来读取或写入任何寄存器。

    不要将uops三元组与解码组混淆。译码组可以生成1到6uops,即使译码组有3条指令并生成3 uops,也不能保证这3uops会一起进入RAT。

   译码器和RAT之间的队列很短(10uops),您不能假定寄存器读取停顿不会使译码器停顿,或者译码器吞吐量的波动不会使RAT停顿。

   除非队列为空,否则很难预测哪些uops将一起通过RAT,并且对于优化过的代码,只有在分支预测错误之后,队列才清空。由同一条指令生成的几个uops不一定会同时通过RAT。 ?ops只是简单的从队列中连续取出,一次三个。 序列不会被预测正确的jump打断,而是jump前后的uops可以同时通过RAT。 只有jump错误预测才会丢弃队列,然后重新开始,以便接下来的三个uops一定会一起进入RAT。

    可以通过性能监视器计数器(PMC)号0A2H来检测寄存器读取的停顿,不幸的是,无法将其与其他类型的资源停顿区分开。这里0A2我个人不太明白是怎么会事,难道是pmc使用这个硬件号来监控一切停顿?
这里的0A2H,我在Intel? 64 and IA-32 Architectures Software Developer’s Manual中找到了。


这是手册中关于intel P6关于性能计数器的描述章节中截取出来的,P6对应的是PPro、Pii和Piii。所以agner描述的就是这个事件号,即可以通过这个事件号来获的stall。

    如果三个连续的μop读取两个以上的不同寄存器,那么你当然希望它们不要一起通过RAT。但是它们一起进入的概率是三分之一。一个三元组中的μops读取三个或四个write-back寄存器的代价是一个时钟周期。您可以将这一个时钟延迟等效于通过RAT再加载三个μops。由于三个μop有1/3的概率一起进入RAT,平均损失将等于3/3 = 1μop。要计算一段代码通过RAT所需的平均时间,请将潜在的寄存器读取停顿数加到μop数上,然后除以3。您可以看到通过添加一条额外的指令来消除停顿是无济于事的,除非您知道哪些μops一起进入RAT,或者您可以通过一条额外的写关键寄存器的指令阻止一个以上的潜在寄存器读取停顿。这里的1/3我不太了解怎么算出来的,但是大体意思是要限制进入RAT的uops很难。但是还是有一些工作可以做的,继续往下看。

    如果您希望每个时钟的吞吐量都为3μop,则每个时钟周期两次永久寄存器读取的限制可能是一个难以解决的瓶颈。但是下面列举的一些措施可能有助于消除寄存器读取停顿:

        1    将读取同一寄存器的μop保持在一起,以便它们可能进入同一三元组;
        2    将读取不同寄存器的μop隔开,以使它们不能进入同一三元组;        
        3    在要写入或修改该寄存器的指令之后,将读取寄存器的μop放置在不超过9-12μop的位置,以确保在读取该寄存器之前没有将其回写(如果之间有一个跳转,则无关紧要只要可以预测)。如果您出于任何原因期望寄存器写入被延迟,则可以在指令流的下方更安全地读取寄存器;
        4    使用绝对地址而不是指针,以减少寄存器读取的次数;
        5    您可以在不会导致停顿的三元组中重命名寄存器,以防止此寄存器在一个或多个以后的三元组中发生读取停顿。这种方法需要额外的μop,因此除非预测的读取平均停顿数超过1/3,否则无效果。

    对于生成多个μop的指令,您可能想知道该指令生成的μop的顺序,以便对寄存器读取停顿的可能性进行精确分析。 因此,下面是最常见的几种情况:

        1    Writes to memory:写memory指令产生2 uops,第一个(到端口4)是存储操作,读取存储内容的寄存器 第二个μop(端口3)计算内存地址,读取其它指针寄存器。着色的内容我感觉不太正确或者理解不到位,感觉也是读取操作,读取要存储着要写入memory的内容的寄存器。如:
            

点击(此处)折叠或打开

  1. ; Example 6.6. Register reads
  2. fstp qword [ebx+8*ecx]
第一个uop 读ST0,第二个uop读ebx和ecx。

        2    Read and modify:读取存储器操作数并通过某些算术或逻辑运算修改寄存器的指令会产生两个μop。 第一个(到端口2)是读取任何指针寄存器的加载指令,第二个μop是读取和写入目标寄存器并可能写入标志的算术指令(到端口0或1)。如:

点击(此处)折叠或打开

  1. ; Example 6.7. Register reads
  2. add eax, [esi+20]
第一个uop读esi, 第二个uop读eax,写eax和标志寄存器。


        3    Read / modify / write:一条读/修改/写指令产生四个μop。 第一个μop(到端口2)读取任何指针寄存器,第二个μop(到端口0或1)读取并写入任何源寄存器,并可能写入标志,第三个μop(到端口4)仅读取临时结果, 不计数,第四个μop(到端口3)再次读取指针寄存器。 由于第一个和第四个μop不能一起进入RAT,因此您无法利用它们读取相同的指针寄存器这一事实。如:

点击(此处)折叠或打开

  1. ; Example 6.8. Register reads
  2. or [esi+edi], eax
第一个μop读取ESI和EDI,第二个μop读取EAX并写入EAX和标志,第三个μop仅读取临时结果,第四个μop再次读取ESI和EDI。 无论这些μop如何进入RAT,都可以肯定读取EAX的μop与两次读取ESI和EDI的μop之一同时工作。 因此,除非可以修改其中一个寄存器,例如通过MOV ESI,ESI,否则该指令将不可避免地发生寄存器读取停顿。 
       
        4    Push register:push寄存器指令产生3μops。 第一个(到端口4)是存储指令,用于读取寄存器 第二个μop(到端口3)生成地址,读取堆栈指针。 第三个μop(到端口0或1)从堆栈指针中减去字长,从而读取并修改堆栈指针。着色内容感觉也因该是读取操作,为何描述成是存储操作?

        5    Pop register:pop寄存器指令产生2μops。 第一个μop(到端口2)加载该值,读取堆栈指针并写入寄存器。 第二个μop(到端口0或1)调整堆栈指针,读取并修改堆栈指针。

        6    Call:一个近调用会产生4μop(端口1、4、3、0/1)。 前两个μop只读取指令指针不计数,因为不能重命名。 第三个μop读取堆栈指针。 最后一个μop读取并修改堆栈指针。

        7    return:一个近return会产生4μop(端口2、0/1、0/1、1)。 第一个μop读取堆栈指针。 第三个μop读取并修改堆栈指针,agner没有说第2和4 uops,估计也是指令指针一类的操作。


6.6 乱序执行

    重排序缓冲区(ROB)可以容纳40μops和40个临时寄存器(图6.1),而reservation station-预留站(RS)可以容纳20μops。 RS在ROB下游,每个μop在ROB中等待,直到其所有操作数准备就绪,并且有一个空的执行单元。 这使得乱序执行成为可能。

    相对于其他写入,对内存的写入不能乱序执行。 写内存有四个写入缓冲区,因此,如果您估计写入时会出现许多高速缓存未命中,或者正在写入未缓存的内存,那么建议您一次安排四个写入,并确保处理器有其他事情要做,然后再进行下一次四个写入。 内存读取和其他指令可能会无序执行,但IN,OUT和序列化指令除外。这里说的四个同时写,并且确保处理器有其它事做,主要是为了高效利用处理器,不必出现由于未命中而等待的情况发生。

    如果代码先写入一个内存地址,之后不久再从同一地址读取,那么由于ROB在重新排序时不知道该内存地址,因此读取可能会在写入之前被错误地执行。 在计算写地址时检测到此错误,然后必须重新执行读操作(投机执行)。 为此,大约需要3个时钟。 避免这种性能损失的最好方法是确保执行单元在同一内存地址的写入和后续读取之间还有其他事情可做。

    根据图6.1,围绕五个端口聚集了多个执行单元。 端口0和1用于算术运算等。简单的移动,算术和逻辑运算可以转到端口0或1,具体转给谁,取决于谁先空闲。 端口0还处理乘法,除法,整数移位和旋转以及浮点运算。 端口1还处理跳转以及一些MMX和XMM操作。 端口2处理所有从内存的读取操作,并执行一些字符串和XMM操作,端口3计算内存写地址,端口4执行所有内存写操作。 由代码指令生成的μop的完整列表以及它们要访问的端口的说明包含在手册4:“指令表”中。 请注意,所有存储器写操作都需要两个μop,一个用于端口3,一个用于端口4,而存储器读取操作仅使用一个μop(端口2)。

    在大多数情况下,每个端口每个时钟周期可以接收一个新的μop。 这意味着,如果有指令进入五个不同的端口,我们可以在同一时钟周期内执行多达5μop,但是由于管道中每个时钟的上限为每个时钟3μop,因此每个时钟周期执行的平均uops数永远不会超过3。

    如果要保持每个时钟3μop的吞吐量,则必须确保没有执行端口接收的μop超过三分之一,这个1/3是指你统计的uops总数的1/3。使用手册4:“指令表”中的μop表并计算每个端口有多少μop。如果端口0和1处于饱和状态而端口2处于空闲状态,则可以通过替换一些寄存器,用MOV寄存器,存储器立即执行的指令来改进代码,以将某些负载从端口0和1移至端口2。

    大多数μop只需一个时钟周期即可执行,但乘法,除法和许多浮点运算则需要更多的时间。浮点加减法需要3个时钟,但是执行单元如果完全流水线化,它可以在前面的每个时钟周期结束之前的每个时钟周期接收新的FADD或FSUB(当然,前提是它们是独立,不能有依赖关系)。

    整数乘法需要4个时钟,浮点乘法需要5个时钟,MMX乘法需要3个时钟。整数和MMX乘法通过流水线传输,可以在每个时钟周期接收一条新指令。浮点乘法是部分流水线的:执行单元可以在前一个时钟之后的两个时钟接收一个新的FMUL指令,因此最大吞吐量为每两个时钟周期一个FMUL。 FMUL之间的漏洞不能用整数乘法填充,因为它们使用相同的执行单元。 XMM加法和乘法分别需要3和4个时钟,并且已完全流水线化。但是,由于每个逻辑XMM寄存器都实现为两个物理64位寄存器,因此一个完整的XMM操作需要两个μop,然后吞吐量将是每两个时钟周期一个算术XMM指令。 XMM加法和乘法指令可以并行执行,因为它们不使用同一执行端口。

    整数和浮点除法最多需要39个时钟,并且没有流水线。 这意味着执行单元在上一个除法运算完成之前无法开始新的除法运算。平方根和超越函数也是如此。

    当然,你更应该尽量避免产生很多μop的指令。 例如,应该用DEC ECX / JNZ XX代替LOOP XX指令。

    如果您有连续的POP指令,则可以将它们分解以减少μop的数量:

点击(此处)折叠或打开

  1. ; Example 6.9a. Split up pop instructions
  2. pop ecx
  3. pop ebx
  4. pop eax
改进为如下,会更好:

点击(此处)折叠或打开

  1. ; Example 6.9b. Split up pop instructions
  2. mov ecx, [esp]
  3. mov ebx, [esp+4]
  4. mov eax, [esp+8]
  5. add esp, 12
前一个代码生成6μop,后者仅生成4μop,并且解译码速度更快。 对PUSH指令执行相同的操作不太有利,因为拆分代码可能会生成寄存器读取停顿,除非您要插入其他指令或最近重新命名了寄存器。 用CALL和RET指令执行此操作将干扰返回堆栈缓冲区中的预测。 还要注意,ADD ESP指令可能会导致早期处理器上的AGI停顿。

6.7 退出

    退出是将μop所使用的临时寄存器复制到永久寄存器EAX,EBX等中的过程。执行μop后,会在ROB中将其标记为准备退出。

    The retirement station(报废站,google是这么翻译的,baidu翻译为退休站,貌似google的人性化一些吧)每个时钟周期可处理3μop。这看起来似乎不是问题,因为在RAT中,吞吐量已限制为每个时钟3μops。但是退出可能仍然是一个瓶颈,原因有两个:
        首先,指令必须按顺序退役,如果某个μop的执行顺序不正确,则它无法在该顺序中的所有先前μop都退出之前退出;
        第二个限制是,jump动作必须在报废站的三个位置中的第一个退出,就像如果下一条指令仅适合D0时,译码器D1和D2可以处于空闲状态一样,如果要退出的下一个μop是跳转,则报废站中的最后两个时隙也可能处于空闲状态。
由此可知,如果您有一个小的循环,其中循环中的μop数能不能被3整除,这非常重要。


    所有μop都保留在重新排序缓冲区(ROB)中,直到它们退出。 ROB可以容纳40μop。这限制了在除法或其他慢速操作的长时间延迟期间可以执行的指令数量。在除法完成之前,ROB可能会被填满。仅当除法完成并退出后,后续的μop才能开始退出,因为退出是按顺序进行的。

    在执行预测分支的情况下,在确定预测正确之前,推测执行的μops不能退出。如果预测结果是错误的,则将以推测方式执行的μop丢弃而不会退出。

    这些指令不能以推测方式执行:内存写入,IN,OUT和序列化指令。

6.8 部分寄存器操作停顿

    部分寄存器停顿是一个问题,当我们写入32位寄存器的一部分并随后从整个寄存器或其中更大的一部分读取时,就会发生停顿。

点击(此处)折叠或打开

  1. ; Example 6.10a. Partial register stall
  2. mov al, byte [mem8]
  3. mov ebx, eax   ; Partial register stall
这将产生5-6个时钟的延迟。 原因是已将临时寄存器分配给AL,以使其独立于AH。 执行单元必须等待,直到对AL的写入已停止,才可以将AL中的值与EAX其余部分的值合并。 可以通过将代码更改为:

点击(此处)折叠或打开

  1. ; Example 6.10b. Partial register stall removed
  2. movzx ebx, byte [mem8]
  3. and   eax, 0ffffff00h
  4. or    ebx, eax
    当然 ,我们也可以通过在写入部分寄存器之后放入其他指令来避免部分停顿,以便在你从完整寄存器读取之前有时间退出。

    所以,每当您混合使用不同的数据大小(8位,16位和32位)时,都应该注意部分寄存器操作引起的停顿。

点击(此处)折叠或打开

  1. ; Example 6.11. Partial register stalls
  2. mov bh, 0
  3. add bx, ax   ; Stall
  4. inc ebx      ; Stall
    在写入完整寄存器或更大部分后读取部分寄存器时,不会产生停顿。

点击(此处)折叠或打开

  1. ; Example 6.12. Partial register stalls
  2. mov eax, [mem32]
  3. add bl, al ;No stall
  4. add bh, ah ;No stall
  5. mov cx, ax ;No stall
  6. mov dx, bx ;stall
    避免部分寄存器操作停顿的最简单方法是在从较小的存储器操作数读取数据时始终使用完整的寄存器并使用MOVZX或MOVSX。 这些指令在PPro,P2和P3上速度很快,但在较早的处理器上速度却很慢。 因此,当您希望您的代码在所有处理器上都能表现得很好时,便会做出妥协,如MOVZX EAX,BYTE [MEM8]一般替换为如下的样子:


点击(此处)折叠或打开

  1. ; Example 6.13. Replacement for movzx
  2. xor eax, eax
  3. mov al, byte [mem8]
    PPro,P2和P3处理器对此组合进行了特殊处理,避免以后从EAX读取数据时寄存器部分停顿。 诀窍是,将寄存器与自身进行异或后,将其标记为空。处理器记住,EAX的高24位为零,因此可以避免部分停顿。 该机制仅适用于某些组合:

点击(此处)折叠或打开

  1. ; Example 6.14. Removing partial register stalls with xor
  2. xor eax, eax
  3. mov al, 3
  4. mov ebx, eax ; No stall

  5. xor ah, ah
  6. mov al, 3
  7. mov bx, ax ; No stall

  8. xor eax, eax
  9. mov ah, 3
  10. mov ebx, eax ; Stall

  11. sub ebx, ebx
  12. mov bl, dl
  13. mov ecx, ebx ; No stall

  14. mov ebx, 0
  15. mov bl, dl
  16. mov ecx, ebx ; Stall

  17. mov bl, dl
  18. xor ebx, ebx ; No stall
通过从寄存器自身中减去寄存器将寄存器设置为零的功能与XOR相同,但是使用MOV指令将其设置为零并不能防止停顿。

我们可以在循环外部设置XOR:

点击(此处)折叠或打开

  1. ; Example  6.15. Removing partial register stalls with xor outside loop
  2.         xor  eax, eax
  3.         mov  ecx, 100

  4. LL:     mov  al, [esi]
  5.         mov  [edi], eax  ; no stall
  6.         inc  esi
  7.         add  edi, 4
  8.         dec  ecx
  9.         jnz  LL
    只要没有中断,错误预测或其他序列化事件,处理器记住EAX的高24位为零。
    记住在调用子程序时,如果可能会用到完整寄存器,必须使任何部分寄存器无效。

点击(此处)折叠或打开

  1. ; Example 6.16. Removing partial register stall before call
  2. add bl, al
  3. mov [mem8], bl
  4. xor ebx, ebx ; neutralize bl
  5. call _highLevelFunction
    许多高级语言在调用过程开始时就压栈EBX,如果您未使BL无效,则会在上面的示例中产生部分寄存器停顿。

    使用XOR方法将寄存器设置为零在PPro,P2,P3和PM上不会破坏其对前面指令的依赖(但对P4确实如此)如:

点击(此处)折叠或打开

  1. ; Example 6.17. Remove partial register stalls and break dependence
  2. div ebx
  3. mov [mem], eax
  4. mov eax, 0          ; Break dependence
  5. xor eax, eax        ; Prevent partial register stall
  6. mov al, cl
  7. add ebx, eax
    在这里将EAX两次设置为零似乎是多余的,但是如果没有MOV EAX,0,则最后一条指令将不得不等待慢速DIV完成,这里是指适当的插入写寄存器,避免多于3次的读寄存器同时进入RAT而导致停顿。而没有XOR EAX,EAX,您将有部分寄存器停顿。

    FNSTSW AX指令很特殊:在32位模式下,它的行为就像写入整个EAX。 实际上,它在32位模式下会执行以下操作:

点击(此处)折叠或打开

  1. ; Example 6.18. Equivalence model for fnstsw ax
  2. and eax, 0ffff0000h
  3. fnstsw [temp]
  4. or eax, [temp]
因此,在32位模式下执行此指令后,读取EAX时不会出现部分寄存器停顿的情况:

点击(此处)折叠或打开

  1. ; Example 6.19. Partial register stalls with fnstsw ax
  2. fnstsw ax / mov ebx,eax ; Stall only if 16 bit mode
  3. mov ax,0  / fnstsw ax   ; Stall only if 32 bit mode
这里需要注意的是使用此指令时是在32位模式还是16位模式。

6.8.1 标志寄存器部分操作停顿

标志寄存器也会引起部分寄存器操作停顿的问题:

点击(此处)折叠或打开

  1. ; Example 6.20. Partial flags stall
  2. cmp eax, ebx
  3. inc ecx
  4. jbe xx       ; Partial flags stall
    JBE指令同时读取进位标志和零标志。 由于INC指令更改了零标志,但没有改变进位标志,因此JBE指令必须等待前两条指令退出,才能将CMP指令中的进位标志和INC指令中的零标志组合在一起。 这种情况很可能是汇编代码中的错误,而不是标志的预期组合。 要更正它,请将INC ECX更改为ADD ECX,1。 导致部分标志寄存器停顿的类似错误是SAHF / JL XX。 JL指令测试符号标志和溢出标志,但SAHF不会更改溢出标志。 要更正它,请将JL XX更改为JS XX

    出乎意料的是(与英特尔手册相反),在一条指令修改了标志寄存器的某些位之后,然后仅读取未修改的标志位,我们也会得到部分标志寄存器停顿:

点击(此处)折叠或打开

  1. ; Example 6.21. Partial flags stall when reading unmodified flag bits
  2. cmp eax, ebx
  3. inc ecx
  4. jc xx            ; Partial flags stall
cmp修改进位bit,inc修改0标志bit,jc为jump if carry,即只读取进位bit,产生部分寄存器操作停顿。但是当只读修改后的标志位时,却没有停顿:

点击(此处)折叠或打开

  1. ; Example 6.22. No partial flags stall when reading modified bits
  2. cmp eax, ebx
  3. inc ecx
  4. jz xx      ; No stall
在读取多位或所有标志位(即LAHF,PUSHF,PUSHFD)的指令上可能会发生部分标志寄存器操作停顿。以下指令后跟随这LAHF或PUSHF(D)指令会导致部分标志寄存器操作停顿:
INC,DEC,TEST,位测试,位扫描,CLC,STC,CMC,CLD,STD,CLI,STI,MUL,IMUL和所有移位和(rotate)反转
。不了解rotate的百度搜索“bit rotate”
以下指令不会导致部分标志寄存器操作停顿:

AND,OR,XOR,ADD,ADC,SUB,SBB,CMP,NEG。
奇怪的是,TEST和AND的行为不同,而根据定义,它们对标记执行的操作完全相同。
您可以使用SETcc指令代替LAHF或PUSHF(D)来存储标志的值,以免发生停顿。
如:

点击(此处)折叠或打开

  1. ; Example 6.23. Partial flags stalls
  2. inc eax   / pushfd                  ; Stall
  3. add eax,1 / pushfd                  ; No stall

  4. shr eax,1 / pushfd                  ; Stall
  5. shr eax,1 / or eax,eax / pushfd     ; No stall

  6. test ebx,ebx / lahf          ; Stall
  7. and  ebx,ebx / lahf          ; No stall
  8. test ebx,ebx / setz al       ; No stall

  9. clc / setz al                ; Stall
  10. cld / setz al                ;No stall
部分标志寄存器操作停顿大约为4个时钟周期。

6.8.2 移位和位反转后的标志寄存器操作停顿(Flags stalls after shifts and rotates)

    当移位或位反转后读取任何标志位时,都会产生标志寄存器操作停顿,但是有一个例外,就是移位和位反转指令后的参数为1时不会产生停顿。

点击(此处)折叠或打开

  1. ; Example 6.24. Partial flags stalls after shift and rotate
  2. shr eax,1 jz xx                                         ; No stall
  3. shr eax,2 jz xx                                         ; Stall 
  4. shr eax,2 / or eax,eax / jz xx                  ; No stall

  5. shr eax,5 / jc xx              ; Stall
  6. shr eax,4 / shr eax,1 / jc xx  ; No Stall

  7. shr eax,cl / jz xx          ; Stall, even if cl = 1
  8. shrd eax,ebx,1 / jz xx      ; Stall
  9. rol ebx,8 / jc xx           ;Stall

和部分标志寄存器操作停顿一样,大约为4个时钟周期。


6.9 存储前向停顿

    存储前向停顿在某种程度上类似于部分寄存器停顿。 当你在相同的内存地址进行混合数据大小不同的操作时,会发生这种情况,即先store再read时候,如果同一地址上进行大小不同的操作时会发生停顿。

点击(此处)折叠或打开

  1. ; Example 6.25. Store-to-load forwarding stall
  2. mov byte [esi], al
  3. mov ebx, dword [esi]; Stall. Big read after small write
byte写入[esi],然后dword读[esi]byte写操作会延迟之后的dword读操作,为此付出的损失约为7-8个时钟周期。

    与部分寄存器操作停顿不同,当你将位宽较大的操作数写入内存然后读取其中的一部分时(如果位宽较小的部分不是从同一地址开始),也会遇到存储前向停顿:

点击(此处)折叠或打开

  1. ; Example 6.26. Store-to-load forwarding stall
  2. mov dword [esi], eax
  3. mov bl, byte [esi]     ; No stall
  4. mov bh, byte [esi+1]   ; Stall. Not same start address
    我们可以通过类似与mov bh, ah这样的操作来避免这种停顿,但是这样的操作对于下面的情况就无能为力了:

点击(此处)折叠或打开

  1. ; Example 6.27. Store-to-load forwarding stall
  2. fistp qword [edi]
  3. mov eax, dword [edi]
  4. mov edx, dword [edi+4]    ; Stall. Not same start address
    有趣的是,在写入和读取完全不同的地址时,这2个地址都使用相同的设置值,但是在不同的bank中,您就会得到一个伪存储前向停顿。

点击(此处)折叠或打开

  1. ; Example 6.28. Bogus store-to-load forwarding stall
  2. mov byte [esi], al
  3. mov ebx, dword [esi+4092]  ; No stall
  4. mov ecx, dword [esi+4096]  ; Bogus stall
这里,我描述一下我自己的理解,首先这个伪存储前向停顿并不是和上面的完全相同,但是也算到这一类,所以加上“伪”字。奔腾处理器从94年推出,到96年PPro,97年PMMX,97-99年依次推出Pii-Piii。在PPro上,一级cache为8k d-cache和8k i-cache。2路bank,每bank大小4k,虽然后来cache容量有所增加,但是bank大小没啥太大变化。所以这里的[esi+4092][esi+4096]应该分配在2个bank上。按照anger的描述,造成这种停顿产生有下面几个条件:
        1    首先store和load的位宽大小不一样;
        2    指令中都使用相同的设定值,这里是esi;
        3    store和load必须在不同的bank中,这里第一个mov的目标地址和第三个mov的目标地址在不同的bank中;
第二个mov目标地址和第一个mov目标地址在一个bank,所以没有stall,而第三个mov目标地址和第一个mov目标地址在不同的bank,所以有stall。

6.10 PPro,P2,P3的瓶颈


    在为这些处理器优化代码时,分析瓶颈在哪里很重要。如果一个瓶颈优化潜力很有限,则花时间优化这样一个瓶颈是没有意义的。

    如果你预料或者判断代码存在大量缓存未命中的情况,那么你应该重组代码以将最经常用的代码部分保持在一起。

    如果您预料或判断存在许多数据缓存未命中的情况,那么请忽略所有其他内容,而集中精力于如何重组数据,或改变数据结构以减少高速缓存未命中的次数,并避免在数据读取未命中后出现较长的依赖链。

    如果代码中除法很多,请尝试按照手册1:“使用C ++优化软件”和手册2:“使用汇编语言优化子例程”中所述减少它们,并确保处理器在进行除法的过程中还有其他事情要做。

    依赖链会阻碍乱序执行。尝试打破长的依赖链,尤其是当它们包含慢速指令(例如乘法,除法和浮点指令)时,尤其如此。请参见手册1:“使用C ++优化软件”和手册2:“使用汇编语言优化子例程”。


    如果代码中有很多跳转,调用或返回,特别是如果这些跳转的可预测性很差,首先尝试是否可以避免其中的一些。将可预测性很差的条件转移替换为条件移动,前提是条件移动不增加依赖关系。
另外就是使用内联小程序。 (请参见手册2:“使用汇编语言优化子例程”)。


    如果要混合使用不同的数据位宽大小(8、16和32位整数),请注意是否有寄存器部分操作停顿。如果使用PUSHF或LAHF指令,请注意部分标志寄存器操作停顿。避免在移位或位反转超过1后读取测试标志。


    如果你的目标是每个时钟周期3μop的吞吐量,那么请注意在指令获取和译码中可能存在的延迟,尤其是在小循环中。指令译码通常在这些处理器中是优化潜力有限的瓶颈,但是不幸的是,这个因素使优化变得相当复杂。如果要在代码的开头进行修改以改进它,则此改动可能会产生副作用,即移动后续代码的IFETCH边界和16字节边界。边界的这种变化可能会对总时钟数产生无法预料的影响,从而使你所做的更改难以理解。

    每个时钟周期两次永久寄存器读取的限制可能会使你的吞吐量降低到每个时钟周期少于3μop。如果你在寄存器上次被修改后然后频繁读取并且每次操作超过4个时钟周期,则可能发生的就是这种情况。例如,如果你经常使用指针寻址数据但很少修改指针,就会这样。

    每个时钟3μop的吞吐量要求每个执行端口的μop不能超过uops总数的三分之一。

    报废站每个时钟周期可以处理3μop,但是对于jump而言可能会没有明显效果。

PPro Pii Piii的部分完结!!!如果大家发现有问题,即时联系,我做修改,也顺便改进自己的错误理解!!!






阅读(2163) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~