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

6.4.      寄存器重命名

寄存器重命名由图6.1所示的寄存器别名表(RAT)控制。来自解码器的μop通过队列去往RAT,然后到ROB与保留站。RAT每时钟周期可以处理3个μop。这意味着微处理器的总体吞吐率平均不会超过每时钟周期3个μop。

对重命名次数没有实际的限制。RAT每时钟周期可以重命名三个寄存器,在一个时钟周期里,它甚至可以重命名同一个寄存器三次。

这个阶段还计算IP-相对分支,并把它们发送到BTB0阶段。

6.5.      ROB读

在RAT之后是ROB读阶段,其中重命名寄存器的值如果可用,被保存到ROB项里。每个ROB项最多可有两个输入寄存器及两个输出寄存器。输入寄存器的值有三种可能:

1.      该寄存器最近没有被修改。ROB读阶段从永久寄存器文件读这个值,保存在ROB项里。

2.      这个值最近被修改。新值是一个已经执行但尚未回收的μop的输出。我假设ROB读阶段将从尚未回收的ROB项读这个值,保存在新的ROB项里。

3.      值尚未就绪。所需的值是一个已经排队但尚未执行的μop的输出。新值尚未被写入,但一旦就绪它将被执行单元写入新的ROB项。

情形1看起来是问题最少的情形。但相当出人意料,这是在ROB读阶段会导致时延的唯一情形。原因是永久寄存器文件仅有两个读端口。在一个时钟周期里,ROB读阶段可以接受来自RAT的最多3个μop,每个μop可以有两个输入寄存器。这给出了总共6个输入寄存器。如果这6个寄存器是不同的,都保存在永久寄存器文件里,那么通过寄存器文件的2个读端口,将需要3个时钟周期执行6个读。前面的RAT阶段将暂停,直到ROB读再次就绪。如果解码器与RAT之间的队列满了,解码器与指令获取也将暂停。这个队列大约仅有10个项,因此它将很快填满。

除了指令只写的寄存器,永久寄存器读的限制适用于一条指令使用的所有寄存器。例如:

;Example 6.4a. Register read stall

mov[edi + esi], eax

movebx, [esp + ebp]

第一条指令产生两个μop:一个读EAX,一个读EDI与ESI。第二条指令产生一个读ESP与EBP的μop。EBX不算作读,因为它仅被该指令写入。让我们假设这三个μop一起通过RAT。对一组3个一起通过RAT的连续μop,我将称之为三元组(triplet)。因为每时钟周期ROB仅能处理两个永久寄存器读,而我们需要5个寄存器读,在它来到保留站(RS)之前,我们的三元组将被推迟两个时钟周期。在三元组有3或4个寄存器读时,它将被推迟一个时钟周期。在同一个三元组里,可以多次读相同的寄存器,而不增加计数。如果上面的指令被改为:

;Example 6.4b. No register read stall

mov[edi + esi], edi

movebx, [edi + edi]

那我们将仅需要两个寄存器读(EDI与ESI),三元组不会被推迟。

情形2与3不会导致寄存器读暂停。如果寄存器还没通过ROB回写阶段,ROB读可以无需暂停读取它。从RAT到达ROB回写,至少需要3个时钟周期,因此可以确定在一个μop三元组中写的寄存器至少可以在后三个三元组中无时延地读。如果回写被重排、慢的指令、依赖链、缓存不命中或任何其他类型的暂停所推迟,那么寄存器可以在指令流更深的地方无延迟地读。例如:

;Example 6.5. Register read stall

moveax, ebx

subecx, eax

incebx

movedx, [eax]

addesi, ebx

addedi, ecx

这6条指令每个产生一个μop。让我们假设头3个μop一起通过RAT。这3个μop读寄存器EBX,ECX与EAX。但因为我们在读EAX之前先写入,这个读是不受约束的,没有暂停。接下来3个μop读EAX,ESI,EBX,EDI与ECX。因为EAX,EBX与ECX在之前的三元组中已经被修改,但尚未回写,它们可以被自由地读取,因此仅计入ESI与EDI,在第二个三元组里我们也不会暂停。如果将第一个三元组中的SUBECX, EAX指令改为CMPECX, EAX,不改写ECX,在第二个三元组中读ESI,EDI与ECX将有一次暂停。类似的,如果将第一个三元组中的INCEBX指令改为NOP或别的,在第二个三元组里读ESI,EBX与EDI将有一次暂停。

要统计寄存器读的次数,你必须包括所用被指令读的寄存器。这包括整形寄存器,标记寄存器,栈寄存器,浮点寄存器及MMX寄存器。一个XMM寄存器算作两个寄存器,除非仅使用了部分,比如在ADDSSS与MOVHLPS里。段寄存器与指令指针不算在内。例如,在SETZAL中,计入标记寄存器,但不计入AL。ADDEBX, ECX算上EBX与ECX,但不算标记寄存器,因为它们仅被写入。PUSHEAX读EAX与栈指针,然后写入栈指针。

FXCH指令是一个特殊情形。它通过重命名工作,但不读取任何值,因此寄存器读暂停规则不把它算在内。就寄存器读暂停规则而言,FXCH指令产生一个既不读、也不写任何寄存器的μop。

不要将μop三元组与解码组混淆。解码组可从1到6个μop产生,即使该解码组有三条指令,并产生3个μop,不保证这3个μop将一起进入RAT。

解码器与RAT之间的队列是如此的短(10个μop),你不能假设寄存器读暂停不会暂停解码器,或者解码器吞吐率里的波动不会暂停RAT。

预测哪些μop一起通过RAT是非常困难的,除非队列是空的,而且对优化的代码仅在误预测分支后这个队列才是空的。同一条指令产生的几个μop不需要同时通过RAT;只是连续从队列中取出μop,一次三个。一个预测正确的跳转不会破坏这个序列:跳转前后的μop可以一起通过RAT。仅误预测的跳转会丢弃这个队列,从头开始,因此接下来3个μop肯定一起进入RAT。

可以通过性能监控计数器0A2H检测寄存器读暂停,但它无法与其他类型的资源暂停区分。

如果3个连续的μop读超过两个不同的寄存器,那么你当然希望它们不要一起通过RAT。一起通过的可能性是三分之一。在一个μop三元组中读3或4个回写寄存器的代价是一个时钟周期。你可以认为一个时钟周期等同于通过RAT载入额外3个μop。由于3个μop一起进入RAT有1/3可能性,平均代价等价于3/3= 1 μop。要计算一段代码通过RAT的平均时间,将潜在的寄存器读暂停数加上μop数,然后除3。可以看到放入一条额外的指令对消除暂停无济于事,除非你确知哪些μop一起进入RAT,或者通过一条写入一个关键寄存器的额外指令,可以避免多个潜在的寄存器读暂停。

在你致力于每时钟周期3个μop的情形下,每时钟周期两个永久寄存器读的限制可能是要处理的问题瓶颈。消除寄存器读暂停的可能方式有:

·        保持读相同寄存器的μop靠在一起,使它们很可能进入相同的三元组。

·        保持读不同寄存器的μop分开,使它们不会进入相同的三元组。

·        将读一个寄存器的μop放在写或修改该寄存器的指令后不超过9-12个μop的位置,确保它在读之前,还没被回写(只要被预测正确,有没有跳转都无关紧要)。如果你有理由预期这个寄存器写出于任何原因被推迟,那么可以安全地在指令流更深的某处读这个寄存器。

·        使用绝对地址而不是指针,以减少寄存器读。

·        你可以在一个三元组中,在不会导致暂停的地方,重命名一个寄存器,以防止在后面一个或多个三元组里对这个寄存器的读暂停。这个方法的代价是一个额外μop,因此除非预期防止的平均读暂停数超过1/3,是得不偿失的。

对产生多个μop的指令,你可以希望知道指令生成μop的次序,以精确分析寄存器读暂停的可能性。因此,我将最常见情形列出如下:

内存写:

一次内存写产生2个μop。第一个(去端口4)是一个储存操作,读取要保存的寄存器。第二个μop(端口3)计算内存地址,读指针寄存器。例如:

;Example 6.6. Register reads

fstpqword ptr [ebx+8*ecx]

第一个μop读ST(0),第二个μop读EBX与ECX。

读与修改

一条读一个内存操作数,通过某种算术或逻辑操作修改一个寄存器的指令产生2个μop。第一个(端口2)是读指针寄存器的内存载入指令,第二个μop是读然后写到目标寄存器,并可能写标记的指令(端口0或1)。例如:

;Example 6.7. Register reads

addeax, [esi+20]

第一个μop读ESI,第二个μop读EAX,然后写EAX与标记寄存器。

读/修改/写

一条读/修改/写指令产生4个μop。第一个μop(端口2)读指针寄存器,第二个μop(端口0或1)读、写源寄存器,并可能写标记寄存器,第三个μop(端口4)仅读不在这里计入的临时结果,第四个μop(端口3)再次读指针寄存器。因为第一与第四μop不会一起进入RAT,你不能利用它们读相同指针寄存器的事实。例如:

;Example 6.8. Register reads

or[esi+edi], eax

第一个μop读ESI与EDI,第二个μop读EAX并写EAX与标记寄存器,第三个μop仅读临时结果,第四个μop再次读ESI与EDI。不管这些μop如何进入RAT,你可以确定读EAX的μop与读ESI与EDI的其中一个μop一起进入。因此,对这条指令一个寄存器读暂停是不可避免的,除非其中一个寄存器最近被修改过,例如被MOVESI, ESI。

寄存器压栈

一条寄存器压栈指令产生3个μop。第一个(端口4)是一条储存指令,读该寄存器。第二个μop(端口3)产生这个地址,读栈指针。第三个μop(端口0或1)从栈指针减去字大小,读且修改栈指针。

寄存器出栈

一条寄存器出栈指令产生2个μop。第一个μop(端口2)载入这个值,读栈指针并写入寄存器。第二个μop(端口0或1)调整栈指针,读且修改栈指针。

调用

一条近程调用产生4个μop(端口1,4,3,01)。头两个μop仅读不计入的指令指针,因为它不能被重命名。第三个μop读栈指针。最后的μop读且修改栈指针。

返回

一个近程返回产生4个μop(端口2,01,01,1)。第一个μop读栈指针。第三个μop读且修改栈指针。

6.6.      乱序执行

重排缓冲(ROB)可以保存40个μop及40个临时寄存器(图6.1),而保留站(RS)可以保存20个μop。每个μop待在ROB里,直到所有的操作数就绪且有空闲的执行单元。这使得乱序执行成为可能。

内存写不能相对于其他写乱序执行。有四个写缓冲,因此如果你预期写会有许多缓存不命中,或者你正在写非缓存内存,那么建议一次安排4个写,并确保在给出下四个写之前,处理器有事可干。内存读与其他指令可以乱序执行,除了IN,OUT以及串行化指令。

如果代码写入内存地址并很快从该地址读,那么因为ROB在重排时不知道该内存的地址,这个读可能在这个写之前被错误执行。在计算写地址时,这个错误被检测出来。然后读操作(它被推测执行)必须重做。对此的惩罚是大约3个时钟周期。避免这最好的方式是确保,在同一个内存地址写与后续读之间,执行单元有其他事情可做。

几个执行单元围绕5个端口聚集在一起。端口0与1用于算术操作等,简单的移动,算术与逻辑操作可以去往端口0或1,取决于谁先空下来。端口0还处理乘法、除法、整数偏移与旋转,以及浮点操作。端口1还处理跳转与某些MMX及XMM操作。端口2处理所有的内存读,以及少数字符串与XMM操作,端口3计算内存写的地址,端口4执行所有内存写操作。由代码指令产生的μop的完整列表,以及它们去往的端口,包含在手册4“指令表”中。注意所有的内存写操作要求两个μop,一个用在端口3,一个在端口4,而内存读操作仅使用一个μop(端口2)。

在大多数情形里,每个端口每时钟周期可以接受一个新的μop。这意味着在同一个时钟周期里可以执行最多5个μop,如果它们去往5个不同的端口,不过因为在流水线更早的地方有每时钟周期3个μop的限制,平均来说每时钟周期不会执行超过3个μop。

如果你希望维持每时钟周期3个μop的吞吐率,你必须确保没有执行单元接收超过三分之一的μop。使用手册4“指令表”中的μop表,计算每个端口有多少μop。如果端口0及1被饱和了,而端口2空闲,那么你可以通过MOVregister, memory替换某些MOV register, register或MOVregister, immeidate指令,将一部分读从端口0与1移到端口2。

大多数μop仅需要一个时钟周期来执行,但乘法、除法以及许多浮点操作需要更多。浮点加法与减法需要3个时钟周期,但执行单元是完全流水线化的,因此在前面的完成之前,它可以在每个时钟周期接受一个新的FADD或FSUB(当然,只要它们无关的)。

整数乘法需要4个时钟周期,浮点乘法5个,MMX乘法3个。整数与MMX乘法是流水线化的,因此每时钟周期可以接受一条新指令。浮点乘法是部分流水线化的:执行单元在前一条指令开始的2个时钟周期后可以接受一条新FMUL指令,因此最大吞吐率是每两时钟周期一条FMUL。FMUL之间的空隙不能由整数乘法填充,因为它们使用相同的执行单元。XMM加法与乘法分别需要3与4个时钟周期,且完全流水线化。但因为每个逻辑XMM寄存器被实现为两个物理64位寄存器,一个封装的XMM操作需要2个μop,吞吐率将是每两时钟周期一个算术XMM指令。XMM加法与乘法指令可以并行执行,因为它们使用不同的端口。

整数与浮点除法最大需要39个时钟周期,且没有流水线化。这意味着直到前一个除法完成,执行单元才能开始新的除法。这也适用于平方根与三角函数。

当然,你应该避免产生许多μop的指令。例如LOOPXX指令,应该由DECECX / JNZ XX替代。

如果有连续的POP指令,可以打破它们,以减少μop的数量:

;Example 6.9a. Split up pop instructions

popecx

popebx

popeax

可以改为:

;Example 6.9b. Split up pop instructions

movecx, [esp]

movebx, [esp+4]

moveax, [esp+8]

addesp, 12

前者产生6个μop,后者仅4个且解码更快。对PUSH指令这样做好处没那么大,因为分散的代码很可能产生寄存器读暂停,除非在它们之间有其他指令,或者寄存器最近被重命名了。对CALL及RET指令这样做将干扰返回栈缓冲里的预测。还要注意,在较早的处理器上,ADDESP指令会导致一个AGI暂停。

6.7.      回收

回收是μop使用的临时寄存器被拷贝到永久寄存器EAX、EBX等的过程。在一个μop被执行后,在ROB中它被标记为准备回收。

回收站每时钟周期可以处理3个μop。这看起来不像是个问题,因为在RAT中吞吐率已经被限制为每时钟周期3个μop。但由于两个原因,回收仍然可能是一个瓶颈。首先,指令必须顺序回收。如果μop被乱序执行,它不能在所有之前的μop顺序回收之前回收。第二个限制是,被采用的跳转必须在回收站三个工位中的第一个里回收。就像D1与D2是空闲的,如果下一条指令仅适合D0,回收站中最后两个工位是空闲的,如果下一个回收的μop是一个被采用的跳转。如果你有一个μop数量不是3倍数的小循环,这一点很重要。

所有的μop都待在重排缓冲(ROB)里,直到回收。ROB可以保存40个μop。这对在长时延的除法或其他慢的操作期间可以执行的指令数,设置了限制。在除法完成之前,ROB将可能充满等待回收的已执行μop。仅当除法完成且回收时,后续的μop开始回收,因为回收依次进行。

在被预测分支推测执行的情形下(参考第11页),推测执行的μop不能被回收,直到确认预测正确。如果预测被证实是错的,那么这些推测执行的μop被丢弃,不回收。

以下指令不能推测执行:内存写,IN,OUT以及串行化指令。

相关文章
相关标签/搜索