5.3. 指令解码
不在追踪缓存的指令将直接从指令解码器去到执行流水线。在这个情形下,最大吞吐率由指令解码器确定。
在大多数情形下,解码器为每条指令产生1-4个μop。对于要求多于4个μop的复杂指令,μop从微代码(microcode)ROM提交。手册4:“指令表”中列出了机器码μops的数目,以及每条指令产生的微代码μops。
解码器最多可以每时钟周期处理一条指令。少数情形下解码一条指令需要多个时钟周期。
产生微代码的指令可能需要多个时钟周期来解码,有时要多得多。以下指令,在某些情形下会产生微代码,不会需要明显更多的时间来解码:自/至段寄存器的移动,ADC,SBB,IMUL,MUL,MOVDQU,MOVUPS,MOVUPD。
带有许多前缀的指令需要额外的解码时间。P4上的指令解码器每时钟周期可以处理一个前缀。因此在P4上,带有多个前缀的指令每个前缀的解码需要一个时钟周期。在不需要段前缀的32位扁平内存模式中,带有多个前缀的指令是罕见的。
P4E上的指令解码器每时钟周期可以处理两个前缀。因此,最多两个前缀的指令可以在一个时钟周期里完成解码,而带有3或4个前缀的指令在两个时钟周期里完成解码。在P4E中引入这个能力,是因为在64位模式中带有两个前缀(即操作数大小前缀与REX前缀)的指令是常见的。多于两个前缀的指令非常罕见,因为在64位模式中段前缀很少使用。
对可以完全进入追踪缓存的小循环,解码时间不重要。如果代码的关键部分对追踪缓存而言太大,或分散在许多小片段中,那么μops会直接从解码器去到执行流水线,解码速率可能是一个瓶颈。2级缓存的效率使你可以安全地假设它以足够的速度将代码提交给解码器。
如果执行一个代码的时间要长于解码时间,追踪可能不在追踪缓存里。对性能这没有坏的影响,因为下一次执行时,可以没有延迟地直接从解码器直接运行代码。这个技术倾向为执行快过解码的代码片段保留追踪缓存。我没有找到微处理器用来决定一段代码是否应该待在追踪缓存的算法,但这个算法看起来相当保守,仅在极端情形下会代码进入追踪缓存。
5.4. 执行单元
当来自追踪缓存或解码器的μop等待执行时,它们被排队。在寄存器重命名与重排后,每个μop通过一个端口到达一个执行单元。每个执行单元有一个或多个专门用于特定操作,比如加法或乘法,的子单元。P4与P4E端口、执行单元以及子单元的结构分别在下面两个表中列出。
端口 | 执行单元 | 子单元 | 速度 | 时延 | 吞吐率倒数(Reciprocal throughput) |
0 | alu0 | add, sub, mov | 双倍 | 0.5 | 0.5 |
logic | 双倍 | 0.5 | 0.5 | ||
store integer | 单倍 | 1 | 1 | ||
branch | 单倍 | 1 | 1 | ||
0 | mov | move and store fp, mmx, xmm | 单倍 | 6 | 1 |
fxch | 单倍 | 0 | 1 | ||
1 | alu1 | add, sub, mov | 双倍 | 0.5 | 0.5 |
1 | int | misc | 单倍 |
|
|
borrows mmx shift | 单倍 | 4 | 1 | ||
borrows fp mul | 单倍 | 14 | > 4 | ||
borrows fp div | 单倍 | 53 - 61 | 23 | ||
1 | fp | fp add | 单倍 | 4 - 5 | 1 - 2 |
fp mul | 单倍 | 6 - 7 | 2 | ||
fp div | 单倍 | 23 - 69 | 23 - 69 | ||
fp misc | 单倍 |
|
| ||
1 | mmx | mmx alu | 单倍 | 2 | 1 - 2 |
mmx shift | 单倍 | 2 | 1 - 2 | ||
mmx misc | 单倍 |
|
| ||
2 | load | all loads | 单倍 |
| 1 |
3 | store | store address | 单倍 |
| 2 |
表5.2. P4的执行单元
端口 | 执行单元 | 子单元 | 速度 | 时延 | 吞吐率倒数(Reciprocal throughput) |
0 | alu0 | add, sub, mov | 双倍 | 1 | 0.5 |
logic | 双倍 | 1 | 0.5 | ||
store integer | 单倍 | 1 | 1 | ||
branch | 单倍 | 1 | 1 | ||
0 | mov | move and store fp, mmx, xmm | 单倍 | 7 | 1 |
fxch | 单倍 | 0 | 1 | ||
1 | alu1 | add, sub, mov | 双倍 | 1 | 0.5 |
shift | 双倍 | 1 | 0.5 | ||
multiply | 双倍 | 10 | 2.5 | ||
1 | int | misc | 单倍 |
|
|
borrows fp div | 单倍 | 63 - 69 | 34 | ||
1 | fp | fp add | 单倍 | 5 - 6 | 1 - 2 |
fp mul | 单倍 | 7 - 8 | 2 | ||
fp div | 单倍 | 32 - 71 | 32 - 71 | ||
fp misc | 单倍 |
|
| ||
1 | mmx | mmx alu | 单倍 | 2 | 1 - 2 |
mmx shift | 单倍 | 2 | 1 - 2 | ||
mmx misc | 单倍 |
|
| ||
2 | load | all loads | 单倍 |
| 1 |
3 | store | store address | 单倍 |
| 2 |
表5.3. P4E的执行单元
进一步解释参见《IntelPentium 4 and Intel Xeon Processor Optimization Reference Manual》。上面的表与Intel手册稍有出入,以计入各种时延。
在满足以下条件时,可以执行一个μop:
· μop的所有操作数已经就绪。
· 一个合适的执行端口就绪。
· 一个合适的执行单元就绪。
· 一个合适的执行子单元就绪。
两个执行单元以双倍时钟速度运行。这是用于整数操作的alu0与alu1。这些单元被高度优化以尽可能快递执行最常见的μops。双倍时钟速度使得这两个单元每半个时钟周期接受一个新μop。一条像ADDEAX, EBX的指令可以在这两个单元之一执行。这意味着执行核心每时钟周期可以处理4个整数加法。Alu0与alu1都流水线化为三个阶段。结果的低半部(在P4E上32位,P4上16位)在前半个时钟周期中计算,高半部在后半个时钟周期中计算,标记在第三个二分之一时钟周期里计算。在P4上,半个时钟周期后,低16位对后续μop可用,使得实际时延仅呈现为半个时钟周期。双倍速执行单元被设计来仅处理最常见的指令,使它们尽可能小。对获得高速度这是必要的。
在alu0及alu1的三流水线阶段中这称为“错列(staggered)加法”,在“TheMicroarchitecture of the Pentium 4 Processor”,Intel Technology journal2001中透露。我无法在实验中确认这,因为8位,16位,32位及64位加法的时延没有区别。
未知浮点与MMX单元是否也使用了错列加法,以及速度几何。参考第47页的讨论。
追踪缓存每时钟周期可以提交大约3个μop到队列。这对执行速度设置了一个限制,如果所有的μop都是可以在alu0及alu1中执行的类型。因此,每时钟周期4个μop的吞吐率仅能在μop在之前较低吞吐率期间(由于慢的指令或缓存不命中)排队时获得。我的测量显示每时钟周期4个μop的吞吐率最大可维持11个连续的时钟周期,如果之前较低吞吐率时期里填满了这个队列。
对每个完整的时钟滴答,每个端口可以收到一个μop。端口0与端口1每半个时钟滴答可以收到一个额外的μop,如果这个额外的μop是派往alu0或alu1的。这意味着如果一个代码序列仅包含去到alu0的μop,那么吞吐率是每时钟周期两个μop。如果μop可去往alu0或alu1,那么这个阶段的吞吐率可以是每时钟周期4个μop。如果所有的μop都去往端口1下的单倍速执行单元,那么吞吐率被限制为每时钟周期一个μop。如果所有的端口与单元被均匀使用,那么在这个阶段的吞吐率可以高达每时钟周期6个μop。
单倍速执行单元每时钟周期可以收到一个μop。某些子单元有更低的吞吐率。例如,FP-DIV子单元在前面的除法没完成前,不能开始新的除法。其他子单元完全流水线化。例如,一个浮点加法需要6个时钟周期,但FP-ADD子单元每时钟周期可以开始一个新的FADD操作。换而言之,如果第一个FADD操作从时间T到T+6。那么第二个FADD可以在T+1开始,在T+7完成,而第三个FADD从T+2到T+8,以此类推。显然,仅当每个FADD操作不依赖前面结果时,这才可能。Μop、执行单元、子单元、吞吐率及时延的细节在手册4:“指令表”里列出。以下例子将展示如何使用这个表进行时间计算。测定P4E的时间:
;Example 5.3. P4E instruction latencies
faddst, st(1) ; 0 - 6
faddqword ptr [esi] ; 6 - 12
第一个FADD指令有6个时钟周期的时延。如果在时间T=0开始,它将在时间T=6结束。第二个FADD依赖第一个的结果。因此,时间由时延,而不是FP-ADD单元的吞吐率,决定。第二个加法将在时间T=6开始,在时间T=12结束。第二个FADD指令生成一个载入内存操作数的额外μop。内存载入去往端口0,而浮点算术操作去往端口1。内存载入μop可与第一个FADD同时在时间T=0,甚至更早开始。如果这个操作数在1级或2级数据缓存里,那么我们可以预期它在需要前就绪。
第二个例子展示如何计算吞吐率:
;Example 5.4. P4E instruction throughput
;Clock cycle
pmullwxmm1, xmm0 ; 0 - 7
paddwxmm2, xmm0 ; 1 - 3
paddwmm1, mm0 ; 3 - 5
paddwxmm3, [esi] ; 4 - 6
128位封装乘法时延是7,吞吐率倒数是2。后面的加法使用不同的执行单元。因此只要端口1空闲,它就可以开始。128位封装加法的吞吐率倒数是2,而64位版本的吞吐率倒数是1。吞吐率倒数也称为发布时延(issuelatency)。吞吐率倒数2表示第二个PADD可以在第一个的2时钟周期后开始。第二个PADD工作在64位寄存器上,但使用相同的执行单元。它的吞吐率是1,意味着第三个PADD可以在一个时钟周期后开始。就像前一个例子,最后的指令产生一个额外的内存载入μop。因为内存载入μop去往端口0,而其他μop去往端口1,内存载入不影响吞吐率。在这个例子中,没有指令依赖前面的结果。结果只有吞吐率,而不是时延,是重要的。我们不知道这4条指令是否以程序中顺序执行,还是被重排了。不过,重排不会影响代码序列的总体吞吐率。