Intel, AMD及VIA CPU的微架构(21)

7.7. 栈引擎

栈指令,比如PUSH,POP,CALL与RET,都修改了栈指针ESP。之前的处理器使用整形ALU来调整栈指针。例如,在P3处理器上,指令PUSH EAX产生3个μop,两个用于保存EAX的值,另一个用于将ESP减4。在PM上,相同的指令仅产生一个μop。两个保存μop被μop融合机制合并为一个,而将ESP减去4的减法由专用于栈指针,称为栈引擎的特殊加法器完成。在流水线中,栈引擎紧接着指令解码器,在乱序执行核之前。每时钟周期栈引擎可以处理3个加法。结果,在一个栈操作后,没有指令需要等待栈指针的更新值。这个技术的一个复杂性是,在乱序执行单元里,可能也需要或修改ESP的值。需要一个特殊机制同步栈引擎与乱序执行核中栈指针的值。栈指针ESPP的逻辑真值,在乱序执行核或永久寄存器文件中作为一个32位值ESPO保存,在栈引擎中作为一个有符号8位偏差值ESPd保存:

ESPP = ESPO + ESPd

栈引擎将偏差值ESPd作为一个偏移放入每个栈操作μop的地址片段,使它在端口2或3的地址计算电路中,可以加到ESPO上。ESPd的值不能放入每个可能使用栈指针的μop,仅是由PUSH,POP,CALL及RET产生的μop。如果栈引擎遇到这些以外的、在乱序执行单元里需要ESP的μop,且如果ESPd不是0,那么它插入一个把ESPd加到ESPO,并将ESPd置0的同步μop。那么后面的μop可以将ESPO用作栈指针ESPP的真值。同步μop在这条指令被解码后生成,完全不会影响解码器。

同步机制可由一个简单的例子来说明:

; Example 7.3. Stack synchronization

push eax

push ebx

mov ebp, esp

mov eax, [esp+16]

这个序列将生成4个μop。假定开始时ESPd是0,第一条PUSH指令将生成一个将EAX写到地址[ESPO-4]并设置ESPd = 4的μop。第二条PUSH指令将产生一个将EBX写到地址[ESPO-8]并设置ESPd = -8的μop。当栈引擎从解码器收到MOV EBP, ESP的μop时,它插入一个将-8加到ESPO的同步μop。同时,将ESPd置0。同步μop出现在MOV EBP, ESP μop之前,因此后面的μop可以把ESPO用作ESPP的真值。最后的指令,MOV EAX, [ESP+16],也需要ESP的值,但这里我们不会有另一个同步μop,因为现在ESPd的值是0,因此同步μop是多余的。

要求ESPO同步的指令包括所有以ESP作为源或目标操作数的指令,比如MOV EAX, ESP,MOV ESP, EAX及ADD ESP, 4,以及把ESP用作指针的指令,比如MOV EAX, [ESP+16]。在一条仅写入ESP的指令前产生一个同步μop看起来是多余的。把ESPd置0应该足够,但这可能将区分MOV ESP, EAX与ADD ESP, EAX的逻辑复杂化。

在ESPd接近溢出时,也要插入一个同步μop。在32次PUSH EAX或64次PUSH AX指令后,ESPd的8位有符号值将溢出。在大多数情形里,在29次PUSH或CALL指令后,将得到一个同步μop,以避免下一个时钟周期从所有三个解码器都给出PHSH指令导致的溢出。在最后三条PUSH指令在同一时钟周期解码的情形下,一个同步μop前最大PUSH指令数是31。对POP与RET指令同样适用(实际上,POP可以比PUSH多一个,因为保存的值是-ESPd,最小的有符号8位数是-128,最大是+127)。

同步μop在端口0与1的两个整形ALU中的任一个处执行。它们与其他μop那样回收。PM有一个用于在误预测分支里取消栈引擎结果的恢复表。

以下例子展示了在一个特定程序流中同步μop如何产生:

; Example 7.4. Stack synchronization

             push 1

             call FuncA

             pop ecx

             push 2

             call FuncA

             pop ecx

             ...

             ...

FuncA PROC NEAR

             push ebp

             mov ebp, esp                    ; Synch uop first time, but not second time

             sub esp, 100

             mov eax, [ebp+8]

             mov esp, ebp

             pop ebp

             ret

FuncA ENDP

在FuncA中,MOV EBP, ESP跟在一个PUSH,一个CALL及另一个PUSH后面。如果ESPd一开始是0,这里它将是-12。在可以执行MOV EBP, ESP之前,需要一个同步μop。SUB ESP, 100与MOV ESP, EBP不需要同步μop,因为在上一个同步μop后没有PUSH或POP。之后,在MOV EBP, ESP前,在对FUNCA的第二次调用中,再次有序列POP / RET / POP / PUSH / CALL / PUSH。现在ESPd已经加到12,再回到0,因此第二次来到这里,我们不需要一个同步μop。如果POP ECX被ADD ESP, 4替换,那么在ADD ESP, 4以及MOV EBP, ESP的第二个实例处需要一个同步μop。如果用MOV DWORD PTR [ESP], 2替换POP ECX / PUSH 2,会发生同样的事情,但替换为MOV DWORD PTR [EPB], 2则不会。

通过将指令分为以下几类,我们可以做出那里会产生同步μop的预测规则:

  1. 使用栈引擎的指令:PUSH,POP,CALL,RET,除了RET n。
  2. 在乱序执行核中使用栈指针的指令,即将ESP作为源,目标或指针的指令,以及CALL FAR,RETF,ENTER。
  3. 在栈引擎及乱序执行核中使用栈指针的指令,即PUSH ESP,PUSH [ESP+4],POP [ESP+8],RET n。
  4. 总是同步ESPO的指令:PUSHF(D),POPF(D),PUSHA(D),POPA(D),LEAVE。
  5. 从不涉及栈指针的指令。

类别1与5的指令序列将不会产生任何同步μop,除非ESPd接近溢出。类别2与5的指令序列将不会产生任何同步μop。类别1指令后类别2的第一条指令将产生一个同步μop,除非ESPd是0。类别3的指令在大多数情形里将产生同步μop。类别4的指令从解码器,而不是从栈引擎产生一个同步μop,即使ESPd = 0。

在每时钟周期3个μop是瓶颈以及执行端口0及1都饱和的情形下,你可能希望使用这个规则减少同步μop数。如果瓶颈在别处,无需关心同步μop。

(文献:S. Gochman等:The Intel Pentium M Processor: Microarchitecture and Performance. Intel Technology Journal, vol. 7, no. 2, 2003)。

7.8. ​​​​​​​寄存器重命名

寄存器重命名由显示在图6.1的寄存器别名表(RAT)及重排缓冲(ROB)控制。来自解码器及栈引擎的μop,通过大约可以保存10个μop的队列进入RAT,然后进入ROB-读与保留站。RAT每时钟周期可以处理3个μop。这意味着微处理器的总体吞吐率平均每时钟周期不会超过3个融合μop。

RAT每时钟周期可以重命名3个寄存器,它甚至可以在一个时钟周期里重命名同一个寄存器三次。

有许多重命名的代码有时会导致难以预测的暂停。我的猜测是在用完临时寄存器时,在RAT中会发生这些暂停。PM有40个临时寄存器。

Core Solo/Duo可以在最多四个临时寄存器中重命名浮点控制字,而Pentium M不能重命名浮点控制字。对C/C++代码中浮点到整数转换的性能,这是重要的。

7.9. ​​​​​​​存器读暂停

像PPro,P2与P3,如第66页解释的那样,PM受同样类型的寄存器读暂停影响。ROB-读阶段每时钟周期可以从永久寄存器读不超过3个不同的寄存器。这适用于所有通用寄存器,栈指针,标记寄存器,浮点寄存器,MMX寄存器及XMM寄存器。一个XMM寄存器算两个,因为它保存为两个64位寄存器。对最近有一个先前μop修改,因此值还没传到ROB-回写阶段的寄存器,没有这个限制。寄存器读暂停的详细解释参考第66页。

之前的处理器每时钟周期仅允许两个永久寄存器读。在PM里这个值增加到3,尽管这还不确定。三个寄存器读端口对防止寄存器读暂停可能不足够,因为μop融合与栈引擎,在PM上许多指令产生更少的μop,但寄存器读没有减少。这使得μop流更紧凑,因此增加了平均每μop寄存器读寄存器的次数。一个融合的μop最多可以有三个输入寄存器,而之前的处理器仅允许每μop两个输入。如果在同一时钟周期里,三个带有三个输入寄存器的融合μop都进入ROB-读阶段,那么我们要读最多9个输入。如果这9个输入寄存器都是不同的,且都在永久寄存器文件里,那么将通过三个寄存器读端口,需要3个时钟周期来读它们。

下面的例子展示了如何消除寄存器读暂停:

; Example 7.5. Register read stalls

inc eax                     ; (1) read eax, write eax

add ebx, eax           ; (2) read ebx eax, write ebx

add ecx, [esp+4]    ; (3) read ecx esp, write ecx

mov edx, [esi]         ; (4) read esi, write edx

add edi, edx            ; (5) read edi edx, write edi

这些指令每条都产生一个通过ROB-读的μop。假设在之前三个时钟周期里没有寄存器被修改。μop三个一批,但不知道哪三个在一起。有三种可能:

  1. (1),(2)与(3)在一起。需要四个寄存器读:EAX,EBX,ECX,ESP。
  2. (2),(3)与(4)在一起。需要四个寄存器读:EBX,ECX,ESP,ESI。(EAX不计入内,因为它已经在前面的三元组中被写入了)。
  3. (3),(4)与(5)在一起。需要四个寄存器读:ECX,ESP,SEI,EDI。(EDX不计入内,因为它在读之前已经被写入了)。

所有这三种可能性都涉及读超过三个最近没被修改寄存器时的暂停。通过在第一条指令前插入MOV ECX, ECX可以消除这个暂停。这将刷新ECX,使我们在情形A,B与C中仅需三个永久寄存器读。如果作为替代,我们选择刷新例如EBX,那么我们将在情形A与B中消除这个暂停,但情形C没有。因此,我们有2/3的可能性消除这个暂停。刷新EDI将有1/3可能性消除这个暂停,因为它仅对情形C起作用。刷新ESP也将在所有三个情形里消除这个暂停,但它将延迟内存操作数的获取大约2个时钟周期。总之,通过刷新用作内存读指针的寄存器来消除寄存器读暂停得不偿失,因为它将延迟内存操作数的获取。如果在上面的例子里刷新ESI,将有2/3的可能性消除一个寄存器读暂停,但将延迟EDX的获取。

在上面例子中的代码之前,如果ESP已经被一个栈操作修改,比如PUSH,使得EPSd不为0(参考第81页),那么栈引擎将在(2)与(3)之间插入同步μop。这将消除寄存器读暂停,但会延迟内存操作数的获取。

7.10. ​​​​​​​执行单元

非融合μop从保留站被提交到5个连接到所有执行单元的执行端口。端口0与1接受所有算术指令。端口2用于内存读指令。内存写指令被解融合为两个分别去往端口3与4的μop。端口3计算地址,端口4执行数据传输。

执行单元的最大吞吐率是每时钟周期5个非融合μop,每个端口一个。在之前的处理器上,因为在RAT及保留站中每时钟周期3个μop的限制,这样一个高吞吐率仅能在保留站充满时的一个短突发中获得。而PM可以维持每时钟周期5个μop,因为RAT中的3个融合μop,在执行端口,可以产生5个非融合μop。当三分之一μop是融合的读-修改指令(端口2与端口0/1),三分之一是融合写指令(端口3与4),三分之一是简单ALU或跳转指令(端口0/1)时,获得最大吞吐率。

执行单元在端口0与1之间分配良好,许多指令可以去往任一端口,哪个空去哪个。因此,在大多数情形下,在大多数时间里,保持两个端口忙碌,是可能的。端口0与1都有一个整形ALU,因此两者都可以处理最常见的整形指令,像移动、加法与逻辑指令。两个这样的μop可以同时执行,每个端口一个。封装整形ALU指令也可以去往两个ALU中的任一个。

整形与浮点指令共享同一个乘法单元及除法单元,但ALU不相同(加法与逻辑)。这意味着PM可以同时执行浮点加法与整数加法,但不能同时执行浮点乘法与整数乘法。

简单指令,比如整数加法有一个时钟周期的时延。整数乘法与封装整数乘法需要3-4时钟周期。乘法单元是流水线化的,因此可以每时钟周期开始一个新乘法。浮点加法与单精度浮点乘法同样适用。双精度浮点乘法使用乘法器单元两次,因此可以每两个时钟周期开始一个新乘法,时延是5。

128位XMM操作被分解我两个64位μop,除非输出是64位的。例如,ADDPD指令生成两个用于两个64位加法的μop,而ADDSD仅生成一个μop。

猜你喜欢

转载自blog.csdn.net/wuhui_gdnt/article/details/82700599