5 Pentium 4 (NetBurst) pipeline
这是文章系列中The
microarchitecture of Intel, AMD and VIA CPUs这篇文章中的第5部分,主要介绍P4的流水线。Agner对于各体系结构的描述并不是按照cpu出现的前后时间来进行的。但是我个人认为按照时间前后顺序来描述更有助于理解新事物出现的前因后果以及发展顺序。所以把这一章提前。不多说了,进入正文。
2000年推出的Intel P4和后来的变种P4E都基于所谓的NetBurst微体系结构,与以前的Intel处理器的设计有很大不同。intel本来对P4寄予厚望的,但是结果发现,此架构的效率比预期的低,所以现在已不再在新设计中使用。但是既然agner在这一章节的笔墨比较多,那我们也一起来看看这所谓的失望的一代处理器究竟是怎么个样子。
NetBurst微体系结构的主要设计目标是获得尽可能高的时钟频率。 这只能通过更长的流水线来实现。 第6代微处理器PPro,P2和P3(下一章描述)是十阶的流水线。 PM是同一设计的改进版本,大约有13阶。 第7代微处理器P4是20级流水线,而P4E甚至还多几阶。 有些阶段甚至仅用于将数据从芯片的一个地方移到另一个地方。
与以前的处理器相比较,P4的一个重要改变是代码缓存被跟踪缓存所取代,跟踪高速缓存主要存放已译码的μop而不是指令。 跟踪高速缓存的优点是消除了译码瓶颈,其中存放的uops可以使指令如同RISC那样执行。 缺点是跟踪缓存中的信息不太紧凑,占用更多的芯片空间。
P4中的乱序执行类似于PPro设计,只是容量更大。 重排序缓冲区中可以包含126条μops。 寄存器读取和重命名没有具体限制,但是最大吞吐量仍然限制为每个时钟周期3μop,并且退出机制的限制与PPro中的限制相同。
5.1 Data cache
片上二级缓存用于代码和数据。 对于不同的型号,二级缓存的大小从256 kb到2 MB不等。 二级缓存组织为8路,每行64个字节。 它以256位宽的全速数据总线连接到中央处理器,效率很高。
片上一级数据缓存为8k或16 kb,8路,每行64字节。 一级数据高速缓存相对较小,通过二级高速缓存的快速访问来补偿。 一级数据缓存使用写透机制而不是回写机制。 这减少了写带宽。
5.2 Trace cache(追踪缓存)
指令在被译码为μop后被存储在追踪缓存中。它不是将指令操作码存储在第1级缓存中,而是存储译码后的μop。一个重要的原因是译码阶段是早期处理器的瓶颈。操作码的长度可以是1到15个字节。确定指令操作码的长度非常复杂。而且我们必须知道第一个操作码的长度才能知道第二个操作码从哪里开始。因此,很难并行确定操作码长度。第6代微处理器每个时钟周期可以解码三个指令。在更高的时钟速度下,这可能会更加困难。如果所有的μop具有相同的大小,则处理器可以并行处理它们,并且瓶颈消失了。这是RISC处理器的原理。缓存μop而不是操作码使P4和P4E可以在CISC指令集上使用RISC技术。追踪缓存中的追踪指的是一串按顺序执行的μop,即使它们在原始代码中不是顺序执行的。这样做的优点是使在高速缓存中跳转所花费的时钟周期数最小,这是使用追踪缓存的第二个原因。其实可能大家看了可能也还是不太明白,我感觉其中最关键的一句话是“如果所有的μop具有相同的大小,则处理器可以并行处理它们”。消除cisc指令长度不一致的弊端,想办法在cisc指令集上发挥risc的指令优势。但是总体而言,uops比源操作码占用更多空间。下表是P4/P4E追踪缓存以及条目大小情况:
追踪缓存被组织为12K个条目,2048行,每行6个条目,8路集联。追踪缓存以clock速度的一半运行,每两个时钟周期最多可传输6μops。
5.2.1 P4追踪缓存:
在P4上,追踪缓存的每个条目有16bits专为数据存储使用,这意味着如果某条uop需要存储多于16bit以上的数据时,那么它将使用2个追踪缓存条目,一条uop到底使用一个还是两个缓存条目,你可以根据下面的规则计算出来,这些规则通过实验数据得出来的。
1 一条uop如果没有立即数,也没有内存操作数,那么它只使用一个追踪缓存条目。
2 一条uop如果只有8bits或16bits立即操作数,也只使用一个追踪缓存条目。
3 一条uop有32bits立即数操作数,但是这个立即数在[-32768,32767]的区间内,那么也只使用一个追踪缓存条目,立即操作数存储为16位有符号整数。
如果操作码包含一个32位常量,那么译码器将检查这个常量是否在允许它表示为16位有符号整数的区间内。如果是的话,那么这条μop只使用一个追踪缓存条目。
4 一条μop如果有32位立即操作数并且在[-32768,32767]的区间外,因此不能表示为16位有符号整数,如果它不能从附近的μop借用存储空间的话,它将使用两个追踪缓存条目。
5 需要额外存储空间的μop可以从它附近不使用自身数据空间的μop借用16位额外的存储空间。几乎没有立即操作数和内存操作数的任何μop都会有一个闲置的16位数据空间供其他μop借用。
需要额外存储空间的μop可以从下一个μop以及它之前的3-5μop中的任何一个借用空间(同一个缓存行中这个具体数字不是2或3的话,就是5),甚至即使不在一个追踪缓存行,也能借用。
如果两条uop之间的任何一个已经使用双倍空间大小或已经把空间借出去,则μop就不能从前一个μop借用空间了。 uop优先从前一个μop借用空间。
6 如果可能,近跳转,调用或条件跳转的位移存储为16位有符号整数。如果位移超出[-32768,32767]的范围,并且根据规则5不能借到额外的存储空间,则使用额外的跟踪缓存条目(超出此范围的位移很少)。
7 如果可能,内存加载或存储μop会将地址或位移存储为16位整数。如果存在基址或索引寄存器,则此整数是带符号的,否则为无符号的。
如果直接地址≥65535或间接地址(即具有一个或两个指针寄存器)的偏移超出[-32768,32767]间隔,则需要额外的存储空间。
8 内存加载μop不能从其他μop借用额外的存储空间。如果16位存储空间不足,则将使用额外的追踪缓存条目,而不管是否有没有机会借用空间。
9 大多数内存存储指令生成两个μop:第一个μop进入端口3,计算存储器地址,第二个μop进入端口0,将数据从源操作数传输到第一个μop计算的存储位置。第一个μop总是可以从第二个μop借用存储空间。
此空间不能借给其它uop,即使空间闲置也不行。
10 一个8位,16位或32位寄存器作为源并且没有SIB字节的存储操作可以包含在单个μop中。 根据上述规则5,这些μop可以从其他μop借用空间。SIB字节是指”Scale-Index-Base“,如基址变址寻址或相对基址变址寻址。这里不知道自己的理解对不对,可能有误。
11 段前缀不需要额外的存储空间。
12 μop不能同时具有内存操作数和立即数操作数。 同时包含这两种操作数的指令将被分成两个或多个μop。 因为任何类型μop都不能使用两个以上的追踪缓存条目。
13.需要两个追踪缓存条目的μop不能跨追踪缓存行边界。如果双存储空间的μop跨追踪高速缓存中的6个条目的边界,则将插入一个空白空间,并且此μop将使用下一个追踪高速缓存行的前两个条目。
加载和存储操作的不同这里要解释一下。我的理论依旧如下:
任何uop都不会有超过2个输入依赖(这里不包括段寄存器),任何一条有2个以上输入依赖的指令都翻译成2个或2个以上的uops。如ADC和不怎么常用的CMOVcc。存储操作MOV [ESI+EDI],EAX有3个输入依赖,因此也是翻译成2条uops,第一条uop是计算[ESI+EDI]的地址,第二条uop把EAX的值存储到第一条uop计算的地址里。为了最常见的存储指令的优化,单uop用于处理不超过1个指针寄存器的情况。是翻译成单uop还是多个uops,是通过译码器来实现的,译码器通过查看指令地址域是否含有SIB字节来判断的。如果存在多于1个指针寄存器,或者存在比例索引寄存器,或者ESP用于基址寄存器,就需要一个SIB字节。而加载指令呢,加载指令永远不会有2个以上的输入依赖。所以大多数情况,加载指令都是单uop执行。与存储uops相比较,加载uops包括更多信息。除了目标寄存器的类型和编号,它还需要存储段前缀、基址指针、索引指针、比例因子和偏移等。但是追踪缓存条目的大小可能必须存放这些信息。所以加载uop需要更多的位来指示它从何处借用的存储空间,这意味着每条uop需要的追踪缓存条目空间更大一些。对于给定物理大小的追踪缓存,这其实意味着存储的条目会少一些。这可能就是内存加载uops不能借用空间的原因。存储指令不存在此问题,因为除非有SIB字节,否则必需的信息已经在两个μop之间分配,因此包含的信息较少。这段内容主要是解释上面第8/9条规则。
P4追踪缓存的一些使用实例如下(只适用于P4):
-
add eax,10000 ; The constant 10000 uses 32 bits in the opcode, but
-
; can be contained in 16 bits in uop. uses 1 space.
-
-
add ebx,40000 ; The constant is bigger than 215, but it can borrow
-
; storage space from the next uop.
-
-
add ebx,ecx ; Uses 1 space. gives storage space to preceding uop.
-
-
mov eax,[mem1] ; Requires 2 spaces, assuming that address ≥ 216;
-
; preceding borrowing space is already used.
-
-
mov eax,[esi+4] ; Requires 1 space. 根据上面规则7的后半部分得出
-
mov [si],ax ; Requires 1 space. 根据上面规则10前半部分得出
-
mov ax,[si] ; Requires 2 uops taking one space each. //这个表示不太明白,根据上面规则7也应该是1个才正确,但是怎么就2了啊??
-
movzx eax, word [si] ; Requires 1 space.
-
movdqa xmm1,es:[esi+100h] ; Requires 1 space.
-
fld qword [es:ebp+8*edx+16] ; Requires 1 space.
-
mov [ebp+4], ebx ; Requires 1 space.
-
mov [esp+4], ebx ; Requires 2 uops because sib byte needed.
-
-
fstp dword [mem2] ; Requires 2 uops. the first uop borrows
-
; space from the second one.
除上述方法外,追踪缓存中不使用其他数据压缩。有很多直接操作内存地址的程序对每个数据访问通常都会使用两个追踪缓存条目,即使所有内存地址都在同一窄范围内。在平面存储器模型中,直接操作的内存地址在操作码中占用32位。汇编程序代码通常会只显示低于65535的地址,但是在微处理器看到它们之前,这些地址被重定位了两次。第一次重定位由链接器完成;第二次重定位在程序加载到内存中的时候由加载程序完成。使用平面内存模型时,加载程序通常会将整个程序放在虚拟地址空间中,该虚拟地址空间的起始值大于65535。所以你可以通过指针访问数据来节省追踪缓存中的空间。在像C++这样的高级语言中,本地数据总是保存在堆栈中并通过指针进行访问。通过使用类和成员函数,可以避免直接寻址全局和静态数据。在汇编程序中可以采用类似的方法。
为避免具有双存储空间的uops跨缓存行(每行6个条目),需要合理安排顺序,如任意两个双存储空间的uops之间安排偶数(包括0)个单空间uops。2-1-2-1这样的连续模式也行。如下:
-
; Example 5.2a. P4 trace cache double entries
-
mov eax, [mem1] ; 1 uop, 2 TC entries
-
add eax, 1 ; 1 uop, 1 TC entry
-
mov ebx, [mem2] ; 1 uop, 2 TC entries
-
mov [mem3], eax ; 1 uop, 2 TC entries
-
add ebx, 1 ; 1 uop, 1 TC entry
我们假设第一个uop从6条目边界,也就是行边界开始,MOV [MEM3],EAX生成的uop需要2个条目,按顺序正好处于第6,7个条目,追踪缓存每行6个条目,也就是说跨行边界。我们可以通过如下调整顺序重新排序来避免这种情况:
-
; Example 5.2b. P4 trace cache double entries rearranged
-
mov eax, [mem1] ; 1 uop, 2 TC entries
-
mov ebx, [mem2] ; 1 uop, 2 TC entries
-
add eax, 1 ; 1 uop, 1 TC entry
-
add ebx, 1 ; 1 uop, 1 TC entry
-
mov [mem3], eax ; 1 uop, 2 TC entries
如果我们没有看到前面的代码,我们也就无法知道最开始的两个μop是否跨越追踪缓存边界,但是我们可以确定MOV [MEM3],EAX生成的μop不会跨越边界,因为最开始的第一个μop的第二个条目不可能是追踪缓存行中的第一个条目。如果安排了较长的代码序列,只要在任何两个双存储空间的μop之间绝不存在奇数个单存储空间的μop,那么我们就不会浪费追踪缓存条目。上面两个示例都假定直接内存操作数大于65535,通常是这种情况。为了简单起见,在这些示例中,我让指令都生成1μop。 对于产生多个μop的指令,你必须分别考虑每个μop。
5.2.2 P4E的追踪缓存:
P4E上的追踪缓存条目容量大于P4上的追踪缓存条目容量,因为处理器可以以64位模式运行。 这大大简化了设计。这完全消除了从相邻条目借用存储空间的需求。每个条目具有32位立即数据存储空间,对于32位模式下的所有μops来说已足够。在64位模式下,只有很少的指令可以具有64位立即数或地址,并且这些指令还会被分为两个或三个μop,每个μop最多包含32个数据位。因此,P4E上,每个μop使用一个,仅一个追踪缓存条目。
5.2.3 追踪缓存分发速度:
追踪高速缓存以时钟速度的一半运行,每两个时钟周期传送一整行追踪高速缓存,即六个条目。这对应于每个时钟周期三个uops的最大吞吐量。在P4上,典型的传输速度可能会略低,因为某些μop使用两个条目,而当两个条目的μop跨越追踪缓存行边界时,某些条目可能会失效。P4E的吞吐量已测量为每个时钟周期精确为8/3或2.667μops。我没有找到任何解释为什么P4E上无法获得每个时钟3μops的吞吐量。这可能是由于管道中其他地方的瓶颈所致。
5.2.4 追踪缓存中的分支处理:
追踪缓存中uops的存储顺序和原始代码的顺序是不同的。如果分支是jump的话,大多数情况会重新组织路径,让完成jump的后继uops在物理位置上排在jump指令后。而不是原始代码顺序中排列在jump后面的代码。这样可以减少路径之间的jump次数。如果由不同的地方jump到同一个地方,追踪缓存中可能会多次出现相同的uops序列。
分支uop后的两条分支路径,有的时候可以通过分支提示前缀来控制其中一条路径。但是我的试验结果是这并不能保证这种优势的一致性。即使在使用分支提示前缀的情况下,这种效果也不会持续很长时间,因为缓存的执行路径会经常重新排列以适合分支μops的行为。因此,您可以假定追踪缓存路径通常是根据分支最常出现的方式进行组织的。这应该是intel P4上的分支预测技术了。不过话说回来,分支预测正确性本来就有概率的。
如果代码中包含许多跳转,调用和分支,则μop分发速度通常小于最大值。如果分支不是追踪缓存行的最后一个条目,并且分支跳转到追踪高速缓存中其他位置,则原追踪高速缓存行中的其余条目虽然已经加载,但是跳转后,也就都没用了。这降低了吞吐量。如果分支μop是追踪缓存行中的最后一个μop,则不会造成任何损失。从理论上讲,可以组织代码,使分支μops出现在追踪高速缓存行的末尾,以避免再次加载uops。但是这样的尝试很少成功,因为几乎不可能预测每个执行路径在追踪缓存中的起点。有时,包含分支的小循环可以通过对其进行重新组织进行优化,使每个都包含许多追踪缓存条目的分支正好落在可被行大小(6)整除的边界。稍微小于行大小倍数的追踪缓存条目的数量的情况要好于略大于行大小倍数的数量的情况。也就是说如果追踪缓存行大小是6个条目的话,某块代码A产生的uops需要的追踪缓存条目数量是 5,10,15...,某块代码B产生的uops需要的追踪缓存条目数量是7,14,21...,则代码模块A要好于代码模块B。显然,这些考虑仅在吞吐量不受执行单元中任何其它瓶颈限制且分支是可预测的情况下才有意义。
5.2.5 改善追踪缓存性能的准则:
A 首选产生uops数量较少的指令;
B 用条件移动替代分支指令,但是确保条件移动不会带来额外的依赖;
C 如果可能,立即数尽量保持在[-32768, 32767]范围内。如果uop具有一个超出此范围的32位立即数,那么最好在这条uop之前或之后紧挨着一条不带立即数也不带内存操作数的uop;
D 避免直接内存操作,如果可以重复使用指针,并且指针寄存器的地址在[-32768, 32767]范围内,则可以通过使用指针来提升性能;
E 避免任何2个双缓存存储空间的uops之间有奇数个单缓存存储空间的uops。一般生成双缓存存储空间的uop的指令包括有直接内存操作数的加载指令,以及其它不满足额外存储空间的uop;
以上的内容只有1和2与P4E相关。后面3条在P4E上不存在。
5.3 Instruction decoding(指令译码)
不在追踪缓存中的指令将直接从指令译码器转到执行单元。 在这种情况下,最大吞吐量由指令译码器决定。
在大多数情况下,译码器会为每条指令生成1-4μops。 对于超过4μops的复杂指令,这些μops是由microcode ROM生成的。手册4:“指令表”中的表格列出了每条指令生成的译码器生成的uops和microcode uops的数量。关于“microcode”,维基这样说:
微指令(英语:microcode),又称微码,是在CISC结构下,运行一些功能复杂的指令时,分解成一系列相对简单的指令。相关的概念最早在1947年开始出现。微指令的作用是将机器指令与相关的电路实现分离,这样一来机器指令可以更自由的进行设计与修改,而不用考虑到实际的电路架构。与其他方式比较起来,使用微指令架构可以在降低电路复杂度的同时,建构出复杂的多步骤机器指令。撰写微指令一般称为微程序设计(microprogramming),而特定架构下的处理器实做中微指令有时会称为微程序(microprogram)。 现代的微指令通常由CPU工程师在设计阶段编写,并且存储在只读存储器(ROM, read-only-memory)或可编程逻辑数组(PLA, programmable logic array)中。然而有些机器会将微指令存储在静态随机存取存储器(SRAM)或是闪存(flash memory)中。它通常对普通程序员甚至是汇编语言程序员来说是不可见的,也是无法修改的。与机器指令不同的是,机器指令必须在一系列不同的处理器之间维持兼容性,而微指令只设计成在特定的电路架构下运行,成为特定处理器设计的一部分。
译码器每个时钟周期以最快一条指令的速度处理指令,在某些情况下,一条指令的译码需要多个时钟周期。以下指令,在特定某些情况下不必花时间去译码,而是由microcode产生,包括moves to/from段寄存器,ADC, SBB, IMUL, MUL, MOVDQU, MOVUPS, MOVUPD。具有多个前缀的指令需要花费更多的时间来译码。P4上的指令译码器可以在每个时钟周期处理一个前缀。 因此,在P4上译码,一条指令带几个前缀就多花费几个时钟周期。在不需要段前缀的32位平面存储器模型中,带有多个前缀的指令很少见。
P4E上的指令译码器可以在每个时钟周期处理两个前缀。 因此,具有最多两个前缀的指令可以在一个时钟周期内译码,而具有三个或四个前缀的指令可以在两个时钟周期内译码。在P4E中引入了此功能,因为在64位模式下带两个前缀的指令是常见的(例如,操作数大小前缀和REX前缀)。带有两个以上前缀的指令非常少见,因为在64位模式下很少使用段前缀。
对于完全被追踪缓存容纳的小循环,译码时间并不重要。如果代码的关键部分对于追踪缓存而言太大,或者散布在许多小块代码中,则uop可能会直接从译码器进入执行单元,并且译码速度很可能会成为瓶颈。这里使用2级缓存非常有效,你可以放心的假设它能以足够快的速度将代码传递给译码器。
如果执行一段代码比译码它花费的时间长,则执行序列可能不会停留在追踪缓存中。这对性能没有负面影响,因为代码可以在下次执行时从译码器直接运行,而不会产生延迟。这种机制倾向表明执行速度比其译码速度快的代码段将保留在追踪缓存。我没有找到微处理器用来决定一段代码是否应该留在追踪缓存中的算法,但是该算法似乎比较保守,仅在极端情况下才拒绝追踪缓存中的代码。
5.4 Execution units(执行单元)
从追踪缓存或译码器出来的uops会排队的等待执行。寄存器重命名和重新排序后,每个μop都会通过一个端口到达执行单元。 每个执行单元具有一个或多个专门用于特定操作的子单元。(例如加法或乘法)。以下两个表分别描述了P4和P4E的端口,执行单元和子单元的组织。
可以在“ Intel Pentium 4和Intel Xeon处理器优化参考手册”中找到进一步的说明。为了描述各种延迟计数,上表与英特尔手册中的图表略有不同。
当满足下面所罗列条件时,一条uop才能被执行:
μop的所有操作数均已就绪;
适当的执行端口已准备就绪;
适当的执行单元已准备就绪;
适当的执行子单元已准备就绪;
ALU0和ALU1这两个执行单元以时钟速度的2倍运行,主要用于整数运算。这两个单元经过深度优化,以尽可能快的速度执行最常见的uops。2倍的时钟速度使得这两个单元每半时钟周期可以接收1条新的uops。像ADD EAX,EBX这样的指令可以在ALU0和ALU1任意一个上面执行。这意味着执行内核每个时钟周期最多能处理4个整形加法。ALU0和ALU1内部都分三阶段执行流水线处理。第一个半时钟周期计算结果的低半部分(P4上16bits,P4E上32bits),第二个半时钟周期计算结果的高半部分,第三个半时钟周期计算标志位。在P4上,在第一个半时钟周期后,低16bits就可以供其它后继uop使用,因此有效等待时间似乎也只有半个时钟周期。2倍时钟速度执行单元最主要目的是用于处理最常见的指令,以使其周期尽量小,这对于cpu高速执行很必要。
英特尔技术期刊2001年文档中的“奔腾4处理器的微体系结构”揭示了alu0和alu1在三阶段流水线中所谓的“错位加法”。由于8位,16位,32位和64位加法之间的延迟没有区别,所以我无法通过实验进行确认。关于浮点和MMX单元是否也使用交叉加法以及以什么速度使用的有关讨论,请参见后面5.5节内容。
追踪缓存可以在每个时钟周期向队列提交大约3 μops。而执行单元alu0和alu1每时钟周期共可处理4 uops,所以如果所有μops都是可以在alu0和alu1中执行的类型,则追踪缓存对执行速度设置了限制。因此,仅当在较低吞吐量(由于指令缓慢或高速缓存未命中)时钟周期期间将uops排队,紧接着后继的时钟周期才能获得每个时钟周期4 uops的吞吐量。我的实验结果显示,如果在较低吞吐量的周期期间队列里的uops已满,则紧挨着的后继执行流,每个时钟周期可获得4μops的吞吐量,但是这种吞吐量最多连续11个时钟周期。这段是说alu0和alu1执行速度较快,一般情况根本获得不了4 uops/clk period,但是如果先让uops队列填满,然后就可以获得4 uops/clk period,但是这种速度也持续不了多久,队列里的uops就不够维持这个吞吐量了。
P4和P4E都有4个执行端口,除端口0和端口1,其它每个端口每个时钟周期都可以接收一个μop,如果μops可用于alu0或alu1,则端口0或端口1在每个半时钟周期可接收一个μop。这意味着,如果代码序列只进入端口2和端口3,这是单速执行单元,吞吐量为每个端口每个时钟周期一个μop,则每个时钟周期的吞吐量为2μop。如果μops可以只进入alu0或alu1,则此阶段的吞吐量是每个时钟周期4μops。如果所有端口和单元均被均匀使用,则此阶段的吞吐量可能会高达每个时钟周期6μop。
单速执行单元每个时钟周期可以接收一个μop。但是由于一些子单元的吞吐量较低。 例如,FP-DIV子单元无法在前一个除法完成之前开始新的除法。其他子单元都完美地流水线化。 例如,浮点加法可能需要6个时钟周期,但是FP-ADD子单元可以在每个时钟周期都开始一个新的FADD操作。换句话说,如果第一个FADD操作从时间T到T+6,则第二个FADD可以在时间T+1开始并在时间T+7结束,第三个FADD从时间T+2到时间T+8,依此类推。显然,只有在每个FADD操作都独立于前一个结果的情况下才有可能。也就是说前后2个FADD不能有依赖关系。
有关μop,执行单位,子单位,吞吐量和延迟的详细信息,请参见手册4:“指令表”。以下示例将说明如何使用此表进行进行时间计算。 下面时序的例子是针对P4E的。
-
; Example 5.3. P4E instruction latencies
-
fadd st0, st1 ; 0 - 6
-
fadd qword [esi] ; 6 - 12
第一条FADD指令的延迟为6个时钟周期。 如果它在时间T = 0处开始,它将在时间T = 6处结束。 第二个FADD取决于第一个的结果。 因此,时间由等待时间决定,而不是由FP-ADD单元的吞吐量决定。 如果第二次加法在时间T = 6开始,则在时间T = 12结束。 第二条FADD指令生成一个附加的μop,用于加载内存操作数。 存储器加载uop进入端口0,而浮点算术运算进入端口1。存储器加载μop可以与第一个FADD同时或更早地在时间T = 0处就开始了。 如果操作数在1级或2级数据高速缓存中,则你可以认为在需要它之前早就已准备就绪。
第2个例子是如何计算吞吐量:
-
; Example 5.4. P4E instruction throughput
-
; Clock cycle
-
pmullw xmm1, xmm0 ; 0 - 7
-
paddw xmm2, xmm0 ; 1 - 3
-
paddw mm1, mm0 ; 3 - 5
-
paddw xmm3, [esi] ; 4 - 6
128位packed乘法的等待时间为7,吞吐量倒数为2。后续加法使用不同的执行单元。因此,它可以在端口1空闲时立即启动。128位packed加法运算的吞吐量倒数为2,而64位版本加法运算的吞吐量倒数1。吞吐量倒数也称为发射延迟。吞吐量倒数为2意味着第二个PADD可以在第一个PADD开始之后的2个时钟开始。第二个PADD在64位寄存器上运行,但是使用相同的执行子单元。它的吞吐量为1,这意味着第三个PADD可以在一个时钟后开始。与前面的示例一样,最后一条指令会生成一个额外的存储器加载μop。当内存加载μop进入端口0,而其他μop进入端口1时,内存加载不会影响吞吐量。本示例中的所有指令均不依赖于前条指令的结果。所以,仅吞吐量问题,而和等待时间无关。我们不知道这四个指令是按程序顺序执行还是重新排序。但是,重新排序不会影响代码序列的整体吞吐量。
5.5 Do the floating point and MMX units run at half speed?(浮点和MMX单元是否以clk/2速度运行)
查看手册4:“指令表”中的表,我们注意到64位和128位整数和浮点指令的许多延迟是偶数,尤其是对于P4。 这导致人们猜测MMX和FP执行单元可能以时钟速度的1/2运行。 为了研究这个问题,我提出了四种不同的假设。
1 与P3一样,将128位指令分为两个64位μop。 但是,此假设与P4上的性能监视器计数器可以测量的μop计数不一致;
2 我们可以假设P4有两个64位MMX单元以clk/2速度一起工作。 每个128位μop都将使用两个单元,并分别占用2个时钟周期,如图5.1所示。 64位μop可以使用两个单元中的任何一个,因此独立的64位μop可以在每个时钟周期以1μop的吞吐量执行,假设clk/2速度执行单元可以同时在奇数和偶数时钟上启动。 有相关关系的64位μop的等待时间为2个时钟,如图5.1所示。测得的延迟和吞吐量与上面描述的假设一致。
为了进一步检验该假设,我进行了128位和64位μop序列交替的实验。 按照假设2,64位μop与128位μop不可能重叠,因为128位μop使用两个64位单元。 这样的话,n个128位μop的长序列与n个64位μop的交替序列应占用4·n个时钟,如图5.2所示。但是,我的实验表明,该序列仅需要3·n个时钟。 (我使64位μop有依赖关系,因此它们不能相互重叠)。 因此,假设2是不成立的。
3 我们可以假设内部数据总线只有64位宽的情况下修改假设2,以便在两个时钟周期内将128位操作数传输到执行单元。如果我们仍然假设有两个64位执行单元以clk/2速度运行,那么当128位操作数的前一半到达时,第一个64位单元可以在时间T=0处开始,当操作数的后一半到达时,第二个64位执行单元将在一个时钟后开始(见图5.3)。然后,在第二个64位单元用128位操作数的后半部分结束之前,第一个64位单元将在时间T=2处接受新的64位操作数。如果我们有一个交替的128位和64位μop序列,在时间T=3时,第三个μop(128位)可以从其前半部分操作数开始,使用第二个64位执行单元,第二个操作数,也就是第三个128位uop的后半部分使用第一个64位执行单元从T=4开始。如图5.3所示,这样就可以解释前面我们的测试结果:n个128位μop的序列与n个64位μop的交替需要3个n个时钟。
简单的128位μops的测量延迟不是3个时钟,而是2个时钟。为了解释这一点,我们必须看看有依赖链关系的128位μops如何执行的。 图5.4显示了存在依赖链的128位μop的执行情况。第一个μop从时间T=0到2处理其操作数的前半部分,而从时间T=1到时间3处理操作数后半部分。在时间T=2开始处理第二个μop的前半部分操作数。即使第二个uop后部分操作数直到T=3才准备就绪。因此,这种n个存在依赖关系的128位μop序列将占用2·n + 1个时钟。 最后的1个额外时钟似乎是链中最后一条指令的等待时间的一部分,等待该指令将结果存储到内存中。 因此,出于实际目的,对于简单的128位μop,我们可以使用2个时钟的延迟进行计算。
4 现在假设只有一个64位算术单元全速运行,它具有2个时钟的延迟,并且分两个阶段进行流水线处理,因此它可以在每个时钟周期接收一个新的64位操作数。在这种假设下,仍将交替执行128位和64位μop的序列,如图5.3所示。如果这样假设4的话,则没有实验手段来区分假设3和假设4,因为假设3的2个执行单元完全相同,在2个假设中,所有的输入、输出时序是一样的。假设3下,2个64位执行单元,如果能找到适当的64位的操作只能在2个执行单元中的一个单元执行,也就是说某些极少数的操作可能仅仅在2个执行单元之一中支持,另外一个不支持。并且可以通过实验证明,当这样的指令执行的时候,另外一个执行单元是空的,这样就可以证明假设3是正确的,而假设4是错误的。我系统地搜索可能仅由两个假设单元之一支持的操作。 我发现的唯一候选对象是64位加法PADDQ。 我的实验表明,64位PADDQ MM在MMX-ALU单元中执行,而128位PADDQ XMM在FP-ADD单元中执行。进一步的实验表明,如果有两个64位MMX-ADD单元,那么它们都可以执行PADDQ MM。 这使得假设4比假设3更有可能。
如果假设4是正确的,那么我们有一个问题,那就是怎么解释为什么它需要两级流水线。如果MMX-ALU单元能够在2个时钟周期内完成64位的错位加法运算,那么也就能在1个时钟周期内完成32位加法。这样就有一个难以置信的问题,仅出于少见的PADDQ MM指令的考虑,设计人员将所有MMX指令的等待时间设为2,而不是1。更有可能的解释是,每个加法器都有特定的流水线阶段。 这样解释貌似更合理一些,因此,我认为假设4是最可能的解释。
但是,以下的描述可能被视为假设3更有可能:“英特尔NetBurst微体系结构采用深度流水线设计,可在不同时钟速率下以不同时钟速率运行芯片的不同部分,从而实现某些单元比处理器的标称时钟频率慢一些,某些单元比处理器的标称时钟频率快一些”(《英特尔奔腾4和英特尔至强处理器优化参考手册》,2001年)。与让最慢的单元确定整体时钟频率相比,让不同的单元以不同的速度运行实际上可能是更好的设计决策。 做出此选择的另一个原因可能是考虑减少功耗并优化散热设计。也可能和clk/2速度运行的追踪缓存有关。
128位mmx uops分成2部分64位操作,2部分有依赖关系,这样128位mmx需要4个时钟周期,这对于假设3和假设4都可以合理解释。在80位寄存器上运行的浮点加法和乘法μop的延迟比128位寄存器中类似μop的延迟多一个时钟周期。 在假设3下,额外的时钟周期可以解释为在64位数据总线上传输80位操作数所花费的额外时间。在假设4下,额外的时钟周期可以解释为产生额外的80位精度所需的时间,貌似对于假设4的解释有点勉强,但是也不能完全否定。80位寄存器中的标量浮点运算的吞吐量为1 μop每个时钟周期,而128位寄存器中的标量浮点运算的吞吐量为80位时候吞吐量的一半,即使它们仅使用128位中的32位或64位。这可能是因为目标操作数的其余96或64位(保持不变)是通过执行单元到达新的(重命名的)目标寄存器。
除法的行为与上面描述的有所不同。 除法使用一个单独的除法单元,它使用迭代并且不进行流水线处理。 除法延时可能是奇数,也可能是偶数,因此除法单元很可能是全速运行。 除法使用FP-MUL单元,这意味着FP-MUL单元也可能全速运行。
5.6 Transfer of data between execution units(执行单元之间的数据传输)
如果前后指令有依赖关系,并且后一条指令和前一条指令不在同一执行单元执行,则大多数情况,会有一个额外的时钟延迟。如下(P4E):
-
; Example 5.5. P4E transfer data between execution units
-
; clock ex.unit subunit
-
paddw xmm0, xmm1 ; 0 - 2 MMX ALU
-
psllw xmm0, 4 ; 2 - 4 MMX SHIFT
-
pmullw xmm0, xmm2 ; 5 - 12 FP MUL
-
psubw xmm0, xmm3 ; 13 - 15 MMX ALU
-
por xmm6, xmm7 ; 3 - 5 MMX ALU
-
movdqa xmm1, xmm0 ; 16 - 23 MOV
-
pand xmm1, xmm4 ; 23 - 25 MMX ALU
第一条指令PADDW在端口1下的MMX单元中运行,并且延迟为2。移位指令PSLLW在相同的执行单元中运行,尽管在不同的子单元中。也没有额外的延迟,因此它可以在时间T=2处开始。乘法指令PMULLW在另一个执行单元FP单元中运行,因为MMX执行单元中没有乘法子单元,这样会产生一个时钟周期的额外延迟。即使移位操作在T=4结束,乘法也要等到T=5才能开始。下一条指令PSUBW又需要回到MMX单元执行,因此从乘法运算(pmullw)完成到减法运算(psubw)开始,我们又有一个时钟周期的延迟。POR不依赖于任何前述指令,因此它可以在端口1和MMX-ALU子单元都空闲时立即启动。MOVDQA指令转到端口0下的MOV单元,这使我们在PSUBW完成后又延迟了一个时钟周期。最后一条指令PAND返回端口1下的MMX单元。但是,mov指令后没有延迟。整个序列需要25个时钟周期。这里除了por指令和其它没有依赖关系,其它前后都有,在不同执行单元间切换时,都需要一个时钟周期的延时。mov指令不存在这种情况。
在两个双速单元ALU0和ALU1之间切换没有延迟,但是在P4上,从这些双速单元到任何其他(单速)执行单元都有半个时钟周期的附加延迟。 如下例(P4):
-
; Example 5.6a. P4 transfer data between execution units
-
-
; clock ex.unit subunit
-
-
and eax, 0fh ;0.0 - 0.5 ALU0 LOGIC
-
xor ebx, 30h ;0.5 - 1.0 ALU0 LOGIC
-
add eax, 1 ;0.5 - 1.0 ALU1 ADD
-
shl eax, 3 ;2.0 - 6.0 INT MMX SHIFT
-
sub eax, ecx ;7.0 - 7.5 ALU0/1 ADD
-
mov edx, eax ;7.5 - 8.0 ALU0/1 MOV
-
imul edx, 100 ;9.0 - 23.0 INT FP MUL
-
or edx,ebx ;23.0-23.5 ALU0/1 MOV
第一条指令AND在ALU0中执行,T=0处开始。以两倍的速度运行,在时间0.5完成。一旦ALU0空闲,则在时间0.5处开始执行XOR指令。第三条指令ADD需要第一条指令的结果,而不是第二条指令的结果。由于ALU0被XOR占用,因此ADD只能用ALU1。从ALU0到ALU1没有延迟,因此ADD可以在时间T=0.5处开始,同时执行XOR,并在T=1.0处同时结束。 SHL指令在单速INT单元中运行。从ALU0或ALU1到任何其他单元有半个时钟延迟,因此INT单元直到时间T=1.5才能接收ADD的结果。 INT单元无法以单速运行,因此无法在半个时钟的滴答声中开始,因此它将等到时间T=2.0并在T=6.0结束。下一条指令SUB回到ALU0或ALU1执行。从SHL指令到任何其他执行单元有一个时钟延迟,因此SUB指令被延迟到时间T=7.0。在两条双速指令SUB和MOV之后,IMUL在INT单元中运行之前,我们又有半个时钟延迟。再次以单速运行的IMUL无法在时间T=8.5处开始,因此将其延迟到T=9.0。 IMUL之后没有其他延迟,因此最后一条指令可以从T=23.0开始,到T=23.5结束。
改进的方法很,其中一个方法是交换ADD和SHL的顺序:
放不下啦!!!!继续阅读II。
阅读(1400) | 评论(0) | 转发(0) |