5. Pentium 4(NetBurst)流水线
在2000年引入的IntelP4,以及基于称为NetBurst微架构的更新P4E版本,与之前Intel处理器的设计差异很大。这个架构被证明比如预期那么高效,在新设计里已不再使用。
NetBurst微架构的主要设计目标是获取尽可能高的时钟频率。这只能通过加长流水线来实现。第6代微处理器PPro,P2与P3(参见下一章)有一个十步流水线。PM是相同设计的一个改进,有大约13步。第7代微处理器P4有一个20步流水线,P4E甚至还多了几步。某些步骤只是将数据从芯片的一部分移动到另一部分。
与之前处理器一个重要的区别是,一个保存已解码μop而不是指令的追踪缓存替代了代码缓存。追踪缓存的好处是,消除了解码瓶颈,设计可以使用RISC技术。坏处是,追踪缓存中的信息不那么紧凑,需要更多芯片空间。
在P4中的乱序核类似于PPro的设计,但更大。重排缓冲可以包含126处理中的μop。对寄存器读与重命名没有限制,但最大吞吐率仍然限制在每时钟周期3个μop,回收站的限制与PPro相同。
5.1. 数据缓存
片上2级缓存用于代码与数据。对不同的型号,2级缓存的大小从256kb到2MB。2级缓存被组织为8路,每行64字节。它通过一条连接到中央处理器的256比特宽总线全速运行,相当高效。
1级数据缓存是8或16kb,8路,每行64字节。对2级缓存的快速访问,补偿了1级缓存相对小的尺寸。1级数据缓存使用写穿(write-through)而不是回写(write-back)机制。这降低了写带宽。
1级代码缓存是一个追踪缓存,解释如下:
5.2. 追踪缓存
指令在解码为μop后保存在追踪缓存。在1级缓存里不是保存指令操作码,而是保存已解码μop。这的一个重要原因是,在更早的处理器时,解码阶段是一个瓶颈。操作码长度从1到15个字节。确定指令操作码的长度相当复杂;要知道第二个操作码在哪里开始,我们必须知道第一个操作码的长度。因此,并行确定操作码长度是困难的。第6代微处理器每时钟周期可以解码三条指令。在更高的时钟频率,这可能更困难。如果μop大小都相同,处理器可以并行处理它们,消除瓶颈。这是RISC处理器的原理。缓存μop而不是操作码,使得P4与P4E在一个CISC指令集上可以使用RISC技术。追踪缓存中的一个追踪是顺序执行的一串μop,即使它们在原始代码里不是顺序的。这样的好处是在缓存中花费在跳转上的时钟周期数是最少的。这是使用追踪缓存的第二个原因。
通常μop比操作码更占空间。下表展示了每个追踪缓存项的大小:
处理器 | 指令编码比特 | 立即数或地址比特 | 地址标记比特 | 每项总比特 | 行数 | 每行项数 | 总项数 |
P4 | 21 | 16 | 16 | 53 | 2048 | 6 | 12k |
P4E | 16 | 32 | 16 | 64 | 2048 | 6 | 12k |
表5.1. 每追踪缓存项比特数以及追踪缓存中的项数(这些数字是大致与推测的。参考www.chip-architect.com 2003-04-20)
追踪缓存被组织为2048行,每行6项,8路组相联。追踪缓存以时钟频率的一半运行,每两个时钟周期提交最多6个μop。
在P4上节省追踪缓存的使用
在P4上,每项为数据保留16个比特。这意味着超过16比特数据存储的μop必须使用两个项。通过下面由实验获得的规则,你可以计算一个μop使用一个还是两个追踪缓存项。
1. 没有立即数及内存操作数的μop仅使用一个追踪缓存项。
2. 带有8或16比特立即数的μop使用一个追踪缓存项。
3. 带有一个范围在-32768到+32767的32位立即数的μop使用一个追踪缓存项。立即数保存为一个16位有符号整数。如果一个操作码包含一个32比特常量,解码器将调查这个常量是否在这个范围里,可以被表示为一个16位有符号整数。如果是,那么这个μop可以包含在一个追踪缓存项里。
4. 如果一个μop的32位立即数超出±215,这样它不能被表示为一个16位有符号整数,这个μop将使用两个追踪缓存项,除非它能从一个邻近的μop借到储存空间。
5. 需要额外储存空间的μop可以从一个不需要数据空间的邻近μop借16比特额外空间。几乎所有没有立即数及内存操作数的μop都有空的16位数据空间可借。需要额外储存空间的μop可以从下一个μop以及前面3-5个μop中的任何一个借空间(如果不是一个追踪缓存行中第2或3个,就是第5个),即使它们不在相同的追踪缓存行中。一个μop不能从前面的μop借空间,如果两者间的μop之一借入或借出了空间。空间最好向前面的μop借。
6. 一个近程跳转、调用或条件跳转,如果有可能,被保存为一个16位有符号整数。如果位移超出范围±215,且根据规则5没有额外的储存空间可以借,需要一个额外追踪缓存项(罕见超出这个范围的位移)。
7. 一个内存读或写μop,如果可能,将地址或位移保存为一个16位整数。如果存在一个基址或索引寄存器,这个整数是有符号的,否则为无符号。如果一个直接地址≥216或者一个间接地址(即带有一个或两个指针寄存器)有超出±215范围的位移,需要额外储存空间。
8. 内存读μop不能从其他μop借额外储存空间。如果16比特储存不够,将使用一个额外追踪缓存项,不理会借的机会。
9. 大多数内存写指令产生两个μop:第一个μop,去往端口3,计算内存地址。第二个μop,去往端口0,将数据从源操作数传递到由第一个μop计算的内存位置。第一个μop总是从第二个μop借储存空间。这个空间不能借给其他μop,即使它是空的。
10. 使用一个8, 16或32位寄存器作为源操作数,且没有SIB字节的写操作可以包含在一个μop中。根据上面的规则5,这些μop可以从其他μop借空间。
11. 段前缀不需要额外的储存空间。
12. μop不能同时有内存操作数与立即数。包含两者的指令将被分解为两个或更多的μop。没有μop使用超过两个追踪缓存项。
13. 需要两个追踪缓存项的μop不能跨越追踪缓存行。如果一个占据两项的μop在追踪缓存中跨过一个6项边界,将插入一个空的空间,μop将使用下一个追踪缓存行的头两项。
需要解释读与写操作之间的差别。我的理论如下:没有μop可以有超过2个输入依赖(不包括段寄存器)。任何有超过两个输入依赖的指令可以被分解为两个或更多的μop。例子有ADC及CMOVcc。一条像MOV[ESI+EDI]也有三个输入依赖。因此,它被分解为两个μop。第一个μop计算地址[ESI+EDI],第二个μop将EAX的值保存到计算得到的地址。为了优化最常见的写指令,实现一个单μop版本来处理不超过一个指针寄存器的情形。解码器通过看指令的地址域中是否有一个SIB字节来区分。如果有多个指针寄存器,或者一个比例索引寄存器或ESP作为基址指针,需要一个SIB字节。另一方面,读指令不会有超过两个输入依赖。因此,在最常见的情形里,读指令被实现为单μop指令。读μop需要包含比写μop更多的信息。除了目标寄存器的类型与数量,它需要保存任何段前缀、基址指针、索引指针、比例因子以及位移。追踪缓存项大小可能被选择为恰好足够包含这些信息。为读μop分配多几个比特来表示它从哪里借储存空间,将意味着所有的追踪缓存项会更大。考虑到追踪缓存上的物理限制,这将意味着更少的项。这可能是为什么内存读μop不能借储存空间的原因。写指令没有这个问题,因为需要的信息已经在两个μop间分配了,除非没有SIB字节,因而更少信息需要包含。
下面的例子将展示追踪缓存使用的规则(仅P4):
;Example 5.1. P4 trace cache use
addeax, 10000 ; The constant 10000uses 32 bits in the opcode, but
; can becontained in 16 bits in uop. uses 1 space.
addebx, 40000 ; The constant is biggerthan 215, but it can borrow
; storagespace from the next uop.
addebx, ecx ; Uses 1 space.gives storage space to preceding uop.
moveax, [mem1] ; Requires 2 spaces,assuming that address ≥ 216;
; precedingborrowing space is already used.
moveax, [esi+4] ; Requires 1 space.
mov[si], ax ; Requires 1space.
movax, [si] ; Requires 2 uopstaking one space each.
movzxeax, word ptr[si] ;Requires 1 space.
movdqaxmm1, es:[esi+100h] ; Requires 1 space.
fldqword ptr es:[ebp+8*edx+16] ; Requires1 space.
mov[ebp+4], ebx ; Requires 1 space.
mov[esp+4], ebx ; Requires 2uops because sib byte needed.
fstpdword ptr [mem2] ;Requires 2 uops. the first uop borrows
; space from the second one.
在追踪缓存中,除了上面提到的方法,没有进一步的数据压缩。一个有许多直接内存地址的程序通常将为每个地址访问使用两个追踪缓存项,即使所有的内存地址都在相同的狭窄范围内。在一个扁平内存模型中,一个直接内存操作数的地址在操作码中占据32比特。汇编器清单通常将显示低于216的地址,不过在微处理器看到它们之前,这些地址被重定位两次。第一次重定位由链接器完成;第二次重定位由载入器在程序载入内存时完成。在使用扁平内存模型时,载入器通常将整个程序放在一个虚拟地址空间中,起始位置>216。通过指针访问数据,你可以节省追踪缓存里的空间。在像C++的高级语言里,局部数据总是保存在栈上,通过指针访问。通过使用类及成员函数,避免全局与静态数据的直接访问。类似的方法可能也适用于汇编程序。
通过调度占据两项的μops,使得在任何两个占据两项μops间有偶数个(包括0)占据单项μops,你可以避免占据两项μops跨越6项边界(一个长的、连续的2-1-2-1模式也可以)。例如:
;Example 5.2a. P4 trace cache double entries
moveax, [mem1] ; 1 uop, 2 TC entries
addeax, 1 ; 1 uop, 1 TCentry
movebx, [mem2] ; 1 uop, 2 TC entries
mov[mem3], eax ; 1 uop, 2 TC entries
addebx, 1 ; 1 uop, 1 TCentry
例如,如果我们假定在这里第一个μop在6项边界开始,MOV[MEM3], EAX的μop,将以空项的代价,跨越下一个6项边界。可以通过重排代码避免之:
;Example 5.2b. P4 trace cache double entries rearranged
moveax, [mem1] ; 1 uop, 2 TC entries
movebx, [mem2] ; 1 uop, 2 TC entries
addeax, 1 ; 1 uop, 1 TCentry
addebx, 1 ; 1 uop, 1 TCentry
mov[mem3], eax ; 1 uop, 2 TC entries
只要我们不看前面的代码,我们就不知道头两个μop是否跨越6项边界,但我们可以确定MOV[MEM3], EAX的μop不会跨越边界,因为第一个μop的第二项不能是一个追踪缓存行的第一项。如果一个长代码序列被这样安排,使得在任何两个占据两项μop间没有奇数个占据单项μop,那么我们将不会浪费任何追踪缓存项。前面两个例子假定直接内存操作数超过216,这是常见的情形。为了简单起见,在这些例子里我仅使用产生一个μop的指令。对产生多个μop的指令,你必须分别考虑每个μop。
P4E上追踪缓存的使用
在P4E上追踪缓存项需要比P4上的大,因为处理器可以运行在64位模式。这大大简化了设计。从相邻项借储存空间的需要被完全消除。对立即数,每个项有32比特,对所有在32位模式下的μop足够了。在64位模式里,仅少数几条指令可以有64位立即数或地址,这些指令被分解为2个或3个μop,每个包含不超过32个数据比特。结果,每个μop仅使用一个追踪缓存项。
追踪缓存提交率
追踪缓存以时钟频率一半运行,每两个时钟周期提交一个6项的追踪缓存行。这对应每时钟周期3个μop的最大吞吐率。
在P4上典型的提交率可能稍低,因为某些μop使用两项,当一个占据两项μop跨越一个追踪缓存行边界时,某些项丢失了。
在P4E上,测量得到吞吐率精确为每时钟周期8/3或2.667个μop。我不能解释为什么在P4E上得不到每时钟周期3个μop的吞吐率。可能由于流水线别处的瓶颈。
追踪缓存中的分支
在追踪缓存中的μop不以原始代码的次序保存。如果一个分支μop大多数时间都跳转,追踪通常将被组织为使得这个跳转μop后接跳转到的μop,而不是在原始代码中后接的μop。这减少了追踪间的跳转数。相同的μop序列可以在追踪缓存中出现多次,如果它从不同的地方跳转而来。
有时在一个分支μop后,通过使用分支暗示前缀(参考第18页),控制保存两个分支中的哪个是可能的,但我的实验表明这样做没有保证的好处。即使在使用分支暗示前缀有好处的情形里,这个效果不能持续很长,因为追踪相当频繁地重排来适应分支μop的行为。因此,你可以假定追踪通常根据分支最经常的路线来组织。
Μop提交率通常低于最大值,如果代码包含许多跳转、调用及分支。如果一个分支不是一个追踪缓存行的最后一项,且该分支去到保存在追踪缓存别处的另一个追踪,那么该追踪缓存行里的余下项都被载入而不使用。这降低了吞吐率。如果分支μop是一个追踪缓存行的最后μop,就没有这个损失。理论上,组织代码使得分支μop出现在追踪缓存行的末尾以避免损失是可能的。但尝试这样做很少成功,因为预测每个追踪在哪里开始是几乎不可能的。有时通过使每个分支包含若干数目能被行大小(6)整除的追踪项,可以改进一个包含分支的小循环。略小于行大小倍数的追踪缓存项数好于略多于行大小倍数的项数。
显然,如果吞吐率不受执行单元里其他瓶颈限制,且分支可预测,这些考虑才是有意义的。
提升追踪缓存性能的指引
以下指引可以改进P4上的性能,如果追踪缓存的μop提交是一个瓶颈:
1. 产生少数μop的指令优先
2. 使用条件移动替换分支指令,如果这不意味着额外的依赖、
3. 尽可能将立即数保持在范围-215到+215。如果μop有一个超出此范围的32位立即数,最好在这个带有大操作数的μop之前或紧接一个没有立即数及内存操作数的μop。
4. 避免直接内存地址。使用指针可以提高性能,如果同一个指针可以反复使用,且地址在指针寄存器的±215范围内。
5. 避免在任何占据两项μop之间是奇数个占据一项的μop。产生占据两项μop的指令包括带有直接内存操作数的内存读,以及其他带有未满足额外储存空间的μop。
仅这些指引的前两个适用于P4E。