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

6.      PentiumPro,II与III流水线

6.1.      在PPro,P2与P3中的流水线

来自1995年的PentiumPro是一个装备乱序执行的Intel处理器。其微架构设计相当成功。这个设计已经被进一步发展,跨越许多代,直到今天的处理器——期间在不那么成功的Pentium4或NetBurst架构上绕了点小弯路。


解释PPro,P2与P3流水线Intel的各种手册、教材现在已经没有了。因此,我将在这里解释这个流水线。

图6.1. Pentium Pro流水线

这个流水线显示在图6.1中。流水线分为以下阶段:

BTB0, 1:分支预测。告知哪里获取下一条指令。

IFU0, 1, 2:指令获取单元。

ID0, 1:指令解码器。

RAT:寄存器别名表。寄存器重命名。

ROB Rd:μop重排缓冲读。

RS:回收站。

Prot0, 1, 2, 3, 4:连接到执行单元的端口。

ROB wb:重排缓冲的结果回写。

RRF:寄存器回收文件(Registerretirement file)。

流水线中每个阶段至少需要一个时钟周期。分支预测已经在第14页解释了。流水线中的其他阶段将在下面解释(文献:IntelArchitecture Optimization Manual, 1997)

6.2.      指令获取

从代码缓存以对齐的16字节块获取指令代码到能保存两个16字节块的双倍缓冲(doublebuffer)。双倍缓存的目的是使得解码一条跨越16字节边界(即地址可被16整除)的指令成为可能。从双倍缓冲代码被成块传递给解码器,这个块我称为IFETCH块(指令获取块,Instructionfetch blocks)。IFETCH块最多16字节。在大多数情形里,指令获取单元使每个IFETCH块在一条指令边界开始,而不是16字节边界。不过,指令获取单元需要指令长度解码器告诉它指令边界从哪里开始。如果这个信息不能及时获得,那么它可能在一个16字节边界开始一个IFETCH块。下面将详细讨论这个问题。

双倍缓冲不足以无延迟地处理跳转周围的指令获取。如果包含该跳转的IFETCH块跨过了16字节边界,那么在产生一个有效的IFETCH块之前,双倍缓冲需要保持两个连续的对齐的16字节代码块。这意味着,在最坏的情形下,在一个跳转后第一条指令的解码会延迟2个时钟周期。在包含跳转指令的IFETCH块中,一个16字节边界的代价是1个时钟周期,在跳转后第一条指令中一个16字节边界的代价也是1个时钟周期。指令获取单元每时钟周期可以获取一个16字节块。如果需要多个时钟周期来解码一个IFETCH块,那么使用这个额外时间来预取是可能的。这可以补偿跳转前后的16字节边界的损失。最终的时延汇总在下面的表6.1。

如果在跳转后,双倍缓冲仅够时间获取一个16字节块,那么该跳转后的第一个IFETCH块将与这个块相同,即对齐到16字节边界。换而言之,跳转后的第一个IFETCH块将不会在第一条指令处开始,而是在之前能被16整除的最接近的地址处。如果双倍缓冲有时间载入两个16字节块,那么新的IFETCH块可以跨过16字节边界,且在跳转后的第一条指令处开始。这些规则汇总在下表:

包含跳转的IFETCH块中的解码组数

这个IFETCH块中的16字节边界

跳转后第一条指令中的16字节边界

解码器时延

跳转后第一个IFETCH的对齐边界

1

0

0

0

16

1

0

1

1

指令

1

1

0

1

16

1

1

1

2

指令

2

0

0

0

指令

2

0

1

0

指令

2

1

0

0

16

2

1

1

1

指令

3或更多

0

0

0

指令

3或更多

0

1

0

指令

3或更多

1

0

0

指令

3或更多

1

1

0

指令

表6.1 跳转周围指令的获取

这个表的第一列表示解码一个IFETCH中所有指令需要的时间(解码组在下面解释)。

令的长度从1到5字节。因此,我们确定一个16字节IFETCH块包含了整数条指令。如果一条指令超出了一个IFETCH块,那么它将进入下一个IFETCH块。这个块将在这条指令的第一个字节处开始。因此,在可以生成下一个IFETCH周期,指令获取单元需要知道在每个IFETCH块中最后一条指令在何处结束。这个信息由指令长度解码器给出,它在流水线的IFU2阶段(图6.1)。指令长度解码器每时钟周期可以确定三条指令的长度。例如,如果一个IFETCH块包含10条指令,那么在知道在该IFETCH块中最后一条指令在哪里结束,可以生成下一个IFETCH块之前,需要3个时钟周期。

6.3.      指令解码

指令长度解码

IFETCH块去到指令长度解码器,它确定每条指令的起始。这是流水线中一个非常关键的阶段,因为它限制了可以实现的并行程度。我们希望每时钟周期获取多条指令,每时钟周期解码多条指令,每时钟周期执行多条指令,以获得速度。但在指令有不同长度时,并行解码指令是困难的。在可以开始解码第二条指令前,你需要解码第一条指令来知道它有多长,第二条指令在哪里开始。因此简单的指令长度解码器每时钟周期仅能处理一条指令。在PPro微架构中的指令长度解码器每时钟周期可以确定三条指令的长度,及早地将这个信息反馈给指令获取单元,用于产生让指令长度解码器在下一个时钟周期操作的新IFETCH块。 这是一个相当令人印象深刻的实现,我确信是通过并行推测解码所有16个可能起始字节。

4-1-1规则

在指令长度解码器后,是将指令翻译为μop的指令解码器。有3个解码器并行工作,因此每时钟周期最多可以解码3条指令。在同一个时钟周期里被解码的最多3条指令被称为一个解码组。三个解码器称为D0,D1与D2。D0可以处理所有指令,并且每时钟周期产生最多4个μop。D1与D2仅能处理产生一个μop且长度不超过8字节的简单指令。IFETCH块中第一条指令总是去往D0。下两条指令,如果可能,去往D1与D2。如果应该进入D1或D2的指令,因为产生多个简单或者长度超过8字节,不能被这些解码器处理,那么它必须等到D0空闲。后续指令也被推迟。例如:

;Example 6.1a. Instruction decoding

mov[esi], eax   ; 2 uops, D0

addebx, [edi]   ; 2 uops, D0

subeax, 1          ; 1 uop, D1

cmpebx, ecx     ; 1 uop, D2

jeL1                    ; 1 uop, D0

在这个例子里的第一条指令去往解码器D0。第二条指令不能进入D1,因为它产生多个μop。因此,它被推迟到D0就绪的下一个时钟周期。第三条指令去往D1,因为前面的指令去了D0。第四条指令去往D2。最后的指令去往D0。整个序列需要3个时钟周期解码。通过交换第二与第三条指令,可以促进解码:

;Example 6.1b. Instructions reordered for improved decoding

mov[esi], eax     ; 2 uops, D0

subeax, 1            ; 1 uop, D1

addebx, [edi]     ; 2 uops, D0

cmpebx, ecx      ; 1 uop, D1

jeL1                     ; 1 uop, D2

现在仅需要2个时钟周期解码,因为指令在解码器间更好的分布。

当根据4-1-1模式安排指令时,获得最大解码速度:如果每三条指令产生4个μop,且下两条指令每条产生1个μop,那么解码器可以每时钟周期产生6个μop。一个2-2-2模式给出每时钟周期2个μop的最小解码速度,因为所有2μop指令都去往D0。建议你根据4-1-1规则安排指令,使得每条产生2,3或4个μop的指令后接两条每条产生1个μop的指令。一条产生超过4个μop的指令必须去往D0。它需要2个或更多时钟周期来解码,而且没有其他指令可以并行解码。

IFETCH块边界

更复杂的是,IFETCH块里的第一条指令总是去往D0。如果代码已经根据4-1-1规则调度,而且如果预定给D1或D2的其中一条1-μop指令恰好是一个IFETCH块中第一个,那么该指令去往D0,破坏了4-1-1模式。这将推迟解码一个时钟周期。指令获取单元不能把这个IFETCH块调整为4-1-1模式,因为,我猜,要在两个阶段后才得到关于那条指令生成多个μop的信息。

这个问题难以处理,因为很难猜IFETCH边界在哪里。处理这个问题的最好方式是调度代码,使得解码器每时钟周期可以产生超过3个μop。在流水线中的RAT与RRF阶段(图6.1)每时钟周期可以处理不超过3个μop。如果根据4-1-1规则安排指令,使得我们可以每时钟周期至少预取4个μop,那么我们可以承担每个IFETCH块损失一个时钟周期,而且仍然维持平均解码器吞吐率不小于每时钟周期3个μop。

另一个措施是使得指令尽可能短,以在每个IFETCH块中指令更多。每IFETCH块更多指令意味着更少IFETCH边界,因而更少破坏4-1-1模式。例如,你可以使用指针而不是绝对地址,来减小代码大小。更多关于如何减小指令大小的细节,参考手册2:“Optimizingsubroutines in assembly language”。

在某些情形里,操纵代码使得面向机器码D)的指令落在IFETCH边界。但通常确定IFETCH边界在哪里相当困难,可能不值得付出努力。首先,你需要使代码段段落对齐,以便知道16字节边界在哪里。然后你必须知道希望优化的代码的第一个IEFTCH块在哪里。查看汇编器的输出列表看每条指令有多长。如果你知道一个IFETCH块在哪里开始,然后你可以下面的方式找出下一个IFETCH块开始的位置:使IFETCH块长度为16字节。如果它在一条指令边界结束,那么下一个块将在那里开始。如果它以一个未结束的指令结束,那么下一个块将从这条指令的开头开始。这里仅统计指令的长度,不关心它们产生多少μop或者它们做什么。这样你可以完全凭借代码并标记出每个IFETCH块在哪里开始,努力前进。最大的问题是知道哪里开始。下面是一些指引:

·        跳转、调用或返回后的第一个IFETCH块可以在第一条指令或之前最接近的16字节边界处开始,根据表6.1。如果将第一条指令对齐到在16字节边界开始,那么你可以确定第一个IFETCH块在这里开始。为此,你可能希望对齐重要的子例程入口与循环入口到16。

·        如果两条连续指令的组合长度超过16字节,那么你可以确定第二条指令不能,像第一条那样,放入同一个IFETCH块,结果你将总是有一个在第二条指令处开始的IFETCH块。你可以使用它作为查找后续IFETCH块在哪里开始的起点。

·        分支误预测后的第一个IFETCH块在16字节边界开始。如第14页解释的那样,一个重复超过5次的循环在退出时总是有一次误预测。因此,在这样一个循环后的第一个IFETCH块将在前面最接近的16字节边界处开始。

我想现在你希望得到以下的一个例子:

;Example 6.2. Instruction fetch blocks

地址              指令                                     长度             uops          期望解码器

1000h           mov ecx, 1000                        5                  1               D0

1005h     LL: mov [esi], eax                         2                   2              D0

1007h           mov [mem], 0                       10                  2              D0

1011h           lea ebx, [eax+200]                 6                   1              D1

1017h           mov byte ptr [esi], 0              3                   2             D0

101Ah           bsr edx, eax                            3                   2              D0

101Dh           mov byte ptr [esi+1], 0         4                   2              D0

1021h           dec edx                                    1                   1               D1

1022h           jnz LL                                        2                   1               D2

让我们假设第一个IFETCH块在地址0x1000开始,在0x1010结束。这在指令MOV[MEM], 0结束前,因此下一个IFETCH块将在0x1007开始,在0x1017结束。这是一个指令边界,因此第三个IFETCH块将在1017h开始,覆盖循环的余下部分。解码所需的时钟周期数是D0的指令数,即LL循环每迭代5条。最后的IFETCH块包含三个覆盖最后五条指令的解码块,且有16字节边界(0x1020)。查看上面的表6.1,我们发现在跳转后的第一个IFETCH块将在跳转后第一条指令处开始,这是0x1005处的LL标记,在0x1015处结束。这在LEA指令结束前,因此下一个IFETCH将从0x1011到0x1021,最后一个从0x1021起覆盖余下部分。现在LEA指令与DEC指令都落在一个IFETCH块的开头,这迫使它们去往D0。现在我们在D0中有7条指令,在第二次迭代中,循环需要7个时钟周期解码。最后的IFETCH块仅包含一个解码组(DECECX / JNZ LL)且没有16字节边界。根据表6.1,跳转后的下一个IFETCH块将在16字节边界处开始,即0x1000。这将给予我们与第一次迭代相同的情形,你将看到循环间隔需要5与7个时钟周期进行解码。因为没有其他瓶颈,运行1000次迭代,整个循环将需要6000个时钟周期。如果起始地址不同,使循环的第一条或最后一条指令在16字节边界,那么将需要8000个时钟周期。如果重排循环,使得没有D1或D2指令落在一个IFETCH块的开头,那么你可以做到仅需要5000个时钟周期。

上面的例子是故意构造的,使获取与解码成为仅有的瓶颈。可以做来改进解码的一个件事是改变例程的起始地址,避免你不希望的16字节边界。记住使得代码段段落对齐,使你知道边界在哪里。操纵指令长度将IFETCH边界放在预期的地方是可能的,如手册“Optimizingsubroutines in assembly language”的章节“Making instructions longer forthe sake of alignment”解释的那样。

指令前缀

指令前缀也可以在解码器里导致损失。如手册2“Optimizingsubroutines in assembly languag”所列举,指令可以有几种前缀。

1.       如果指令有一个16或32比特立即数,操作数大小前缀付出了几个时钟周期的代价,因为操作数的长度被这个前缀改变了。例子(32位模式):

;Example 6.3a. Decoding instructions with operand size prefix

addbx, 9                                 ;No penalty because immediate operand is 8 bits signed

addbx, 200                             ;Penalty for 16 bit immediate. Change to ADD EBX, 200

movword ptr [mem16], 9    ; Penalty becauseoperand is 16 bits

最后的指令可以改变为:

;Example 6.3b. Decoding instructions with operand size prefix

moveax, 9

movword ptr [mem16], ax ; No penalty because no immediate

2.      一旦存在一个显式的内存操作数,一个地址大小前缀有代价(即使不存在位移),因为指令代码中的r/m比特被这个前缀修改了。具有隐含内存操作数的指令,比如字符串指令,使用地址大小前缀没有代价。

3.      在解码器中,段前缀没有代价。

4.      在解码器中,重复前缀与锁前缀没有代价。

5.      如果指令有多个前缀,总是有代价的。这个代价通常是每个前缀一个时钟周期。

相关文章
相关标签/搜索