18. AMD Bulldozer,Piledriver与Steamroller流水线

18.1. AMD Bulldozer,Piledriver与Steamroller中的流水线

AMD Bulldozer,Piledriver与Steamroller处理器可以有1到8个计算单元,每个单元两个执行核。每个单元可以运行两个线程或每核一个线程。

指令缓存与获取由一个执行单元中的两个核共享。在Bulldozer与Piledriver中,指令解码器也在两个核间共享,而Steamroller每个核一个解码器。

对每个核,整数执行单元与1级数据缓存是独立的。浮点与向量执行单元以及2级缓存也在一个执行单元的两个核间共享。一个可能的3级缓存在所有计算单元间共享。某些版本有一个集成图形处理单元。

每个核包含4条并行流水线,每时钟周期可以执行最多4条指令。在流水线中,指令被尽可能少、尽可能早地分解。在解码阶段,一条读-修改或读-修改-写指令仅产生一个宏操作。流水线的长度未知。

相比之前的设计,这个设计更关注节能。通过在大多数时间里降低时钟频率,它相当进取地节省电能。某些版本在时钟频率降低时,还降低电压。仅在一长串CPU密集代码后,才能获得最大时钟频率。

18.2. 指令获取

指令获取器在一个执行单元的两个核间共享。指令获取器每时钟周期可以从1级代码缓存获取32个对齐字节的代码。测得获取速度是,在两个核都活动时,每时钟周期最多16字节,在仅一个核活动时,在线性代码中,每时钟周期最多21字节。当指令没有对齐时,获取速度低于这些最大速度。

关键例程入口与循环入口不应该在一个32字节块末尾附近开始。可以将关键项对齐到16字节,或至少确保在一个关键标签后前4条指令中没有16字节边界。

18.3. 指令解码

在代码缓存中,标记了指令边界。每个解码器每时钟周期可以处理4条指令,Bulldozer与Piledriver在每个单元中有一个由两个核共享的解码器。在两个核都活动时,每个核每2个时钟周期得到解码器服务。属于不同核的指令不能在同一时钟周期里解码。仅在每个执行单元运行一个线程时,解码速度是每时钟周期4条指令。

Steamroller每个核有一个解码器,这样每时钟周期它可以解码4条指令,即使在每个单元里运行两个线程时。因此,在Steamroller上,瓶颈很可能是指令获取,而不是解码。

产生两个宏操作的指令称为双指令。Piledriver与Steamroller中的解码器,在一个时钟周期里,可以处理4条单指令(1-1-1-1)或者1条双指令与2条单指令(2-1-1)或者2条双指令(2-2)。Bulldozer可以处理(1-1-1-1)页(2-1-1),但(2-2)不行。

产生超过2个宏操作的指令使用微代码。在生成微代码时,解码器不能做别的事情。这意味着在遇到一条产生超过2个宏操作的指令后,解码器会停止解码几个时钟周期。在Steamroller上,这将仅影响有复杂指令的线程,但在Bulldozer与Piledriver上,将拖延一个单元中的两个线程,因为它们共享同一个解码器。每条指令产生的宏操作数列出在手册4“指令表”里。

带有最多3个前缀的指令可以在一个时钟周期里解码。对有超过3个前缀的指令,有非常大的惩罚。4 ~ 7前缀的指令需要额外14 ~ 15时钟周期来解码。8 ~ 11前缀的指令需要额外20 ~ 22时钟周期,12 ~ 14前缀的指令需要额外27 ~ 28时钟周期。因此,不建议使NOP指令超过3个前缀。这个规则的前缀数包括操作数大小、地址大小、段、重复、锁、REX与XOP前缀。3字节VEX前缀算作1个,而2字节VEX前缀不算入内。转义码(0F,0F38,0F3A)不算入内。

18.4. 循环缓冲

Steamroller在解码器后有一个已解码宏操作队列。每个核一个队列。队列最多可以保存40个宏操作,虽然有时略少。小循环可以绕过解码器,从宏操作队列运行。这节省了能源,消除了微小循环的指令获取瓶颈。在Steamroller上,不超过4条指令的循环每迭代可以仅在1时钟周期里执行,因为它绕过了解码器。

18.5. 指令融合

紧跟一个条件跳转的CMP或TEST指令可融合为单个宏操作。这适用于所有版本的CMP与TEST指令,以及所有条件跳转,除非CMP或TEST有一个rip相对取址,或同时有位移与立即数。例如:

; Example 18.1. Instruction fusion on Bulldozer

test eax,4

jnz L1                                                 ; fused into one op

cmp [Mydata], eax                          ; rip-relative address in 64 bit mode

jb L2                                                   ; not fused if rip-relative address

cmp dword ptr[rsi+8], 2                 ; both displacement and immediate operand

jl L3                                                     ; not fused

cmp [Mydata+rbx*4], eax              ; 32-bit absolute address + scaled index

jg L3                                                    ; fused

dec ecx

jnz L4                                                  ; not fused. Only cmp and test can fuse

(AMD软件优化指引3.06,2012年1月在这里是不正确的)

其他ALU指令不能与条件跳转融合。指令融合不能增加最大解码速率。

18.6. 栈引擎

处理器有一个重命名栈指针的高效栈引擎,但我们不知道它在流水线的何处。

Push,pop及return指令仅使用一个宏操作。就栈指针而言,这些指令有零时延,因此依赖栈指针作为操作数或指针的后续指令不会被拖延。没有观察到在Intel处理器中看到的栈同步μop。

18.7. 乱序调度器

每个核有一个40项的乱序整数调度器,及96个64位寄存器的物理寄存器文件。

共享的浮点单元有自己的60项乱序调度器,以及160个128位寄存器的物理寄存器文件。

对Steamroller,这些数字可能更大,但得不到实际的细节。

18.8. 整数执行流水线

有4条整数执行流水线:

整数流水线

用于

EX0

大多数AL操作,除法

EX1

大多数ALU操作,乘法,跳转

AGLU0

内存读

AGLU1

内存读

表18.1. 整数执行单元

执行流水线EX0页EX1用于大多数整数与通用指令。内存读指令使用AGLU0与AGLU1。内存写指令同时使用AGLU0/1与EX0/1。AGLU0与1还可以处理使用32位与64位通用寄存器的简单寄存器到寄存器移动,除了Bulldozer的早期版本。AGLU0与1不能处理使用8位或16位寄存器或立即数的寄存器移动指令。

在EX0与EX1中,LEA指令作为ALU操作执行。简单LEA指令需要1时钟周期。如果涉及偏移或多个加法,那么需要2时钟周期。如果操作数大小或地址大小是16位,需要一个额外时钟周期。

最多32位的操作数的整数乘法需要4时钟周期,吞吐率是每2时钟周期一个乘法。整数除法没有流水线化。

18.9. 浮点执行流水线

在Bulldozer与Piledriver上,有4个浮点/向量执行流水线,但在Steamroller仅有3个。

浮点流水线

用于

P0

浮点加法,乘法,除法,整数向量乘法

P1

浮点加法,乘法,除法,混排,偏移,封装

P2

整数向量加法,布尔,移动

P3

整数向量加法,布尔,移动,保存

表18.2. Bulldozer与Piledriver浮点执行流水线

浮点流水线

用于

P0

浮点加法,乘法,除法,布尔,整数向量加法,乘法

P1

浮点加法,乘法,除法,混排,偏移,封装

P2

整数向量加法,向量布尔,保存

表18.3. Steamroller浮点执行流水线

所有这些单元可以处理128位操作数。256位操作数被分为2个宏操作。所有浮点加法与乘法需要5时钟周期,如果下一条依赖的指令也是一个加法或乘法,否则可能要6时钟周期。一个融合乘加指令也需要5或6时钟周期。在结果用在相同的单元上,不知道执行单元如何节省1时钟周期。这可能由于更短的数据通路,或者可能执行单元可以节省一个浮点值正规化、格式化或分类的流水线阶段。移动与比较操作与简单整数向量指令的时延是2时钟周期。

大多数执行单元是双倍的,如表18.2与18.3所示,因此吞吐率是每时钟周期是两个128位操作或一个256位操作。通常,宏操作去往先空闲的单元。

储存单元不是双倍的,256位写总数需要多个时钟周期。如果对齐,Bulldozer有最多每3时钟周期1个256位写的吞吐率,如果不对齐,每10时钟周期1个。256位储存,Piledriver特别糟,根据我的测量,吐率是每17时钟周期一个对齐256位写。Steamroller好得多,如果对齐,吞吐率是每2时钟周期一个256位写,如果不对齐是4时钟周期。

对浮点加法,Steamroller的吞吐率低于预期。在单线程应用中,吞吐率仅是每时钟周期一个128位向量,即使有两个128位执行单元用于浮点加法。在运行两个线程上,同时使用这两个加法单元,有时是可能的。

在同一个流水线上混用不同时延的指令很少会导致问题。

3DNow指令不再支持,除了预取指令。

次正规操作数

在一个浮点操作的结果是次正规或下溢时,Bulldozer与Piledriver有大约175额外时钟周期的惩罚,除非激活了flush-to-zero模式。对溢出没有惩罚。Steamroller没有这些惩罚。

融合乘加

浮点单元可以进行类型为d = a * b + c的融合乘加(FMA)指令。Bulldozer支持FMA4指令,其中4个操作数可以是不同的寄存器。Piledriver与Steamroller支持FMA3与FMA4。FMA3指令有3个操作数,其中目标d必须使用与输入操作数a,b或c之一相同的寄存器。对FMA3,Piledriver仅有预期吞吐率的一半,即每时钟周期一个128位向量,但对FMA4是完全吞吐率,即每时钟周期2个128位向量。对FMA3与FMA4,Steamroller都有完全吞吐率。

18.10. AVX指令

对最常见的指令,256位AVX指令的吞吐率一般是每时钟周期一个256位向量,128位指令的吞吐率是每时钟周期2个128位向量。因此,总体吞吐率大致相同,不管使用128位还是256位指令。

在Bulldozer与Piledriver上使用256位指令有几个劣势:

  • 在Bulldozer上,指令解码器每时钟周期不能处理两条双指令。
  • 在Bulldozer与Piledriver上,256位写指令的吞吐率小于128位写指令吞吐率的一半。在Piledriver上,这特别糟,吞吐率是每17 ~ 20时钟周期一个256写。
  • 在Bulldozer与Piledriver上,128位寄存器到寄存器移动有零时延,而256位寄存器到寄存器移动有2时钟周期的时延,加上使用不同域的2 ~ 3时钟周期的惩罚(参考下面)。幸亏在大多数情形下,非破坏性3操作数指令可以避免寄存器到寄存器移动。

因此,在瓶颈是执行单元吞吐率或指令解码时,在Bulldozer与Piledriver上,使用256位指令没有好处。256位写差劲的吞吐率,使得在Piledriver上使用256位寄存器没有优势。在Steamroller上,这些问题已经被解决了。

尽管由于模式切换(参考第119页),Intel处理器对混用256位AVX指令与非AVXXMM指令有重大的惩罚,在这些AMD处理器上,没有这样的惩罚以及明显的模式切换。

AVX指令集提供大多数整数与浮点XMM指令的3操作数版本。这有没有操作数寄存器被改写,因此可以避免大多数寄存器到寄存器移动的好处。除了与旧处理器不兼容,使用这些3操作数指令没有坏处。

18.11. 不同执行域之间的数据时延

来自一个执行单元的输出数据被用作另一个执行单元的输入时,通常有一个时延。不同的执行单元可以被分为6个域(domain),在数据从一个域移动到另一个时,有一个传输时延:

  1. int域。这包括在通用寄存器上的所有操作。
  2. ivec域。这包括所有整数向量操作,以及浮点移动、封装、混排(shuffle)、混合(blend)与布尔操作。
  3. fma域。这包括所有浮点加、减与乘操作,包括融合乘加指令。
  4. fp域。这包括其他浮点指令,比如除法、平方根、取整等。
  5. 读。这包括所有内存读指令。
  6. 写。这包括内存写指令。

这些域之间测得的传输时延如下:

自域

至域

int

ivec

fp

fma

int

0

(10)

n.a.

n.a.

(4)

ivec

(8)

0

1

1

(5)

fp

n.a.

1

0

0

1(+5)

fma

n.a.

1

0

-1

1(+5)

(4)

(6)

(6)

(6)

n.a.

表18.4. 数据传输时延,时钟周期。括号里的数字包括在手册4“指令表”中列出的时延计数中。

注意许多浮点指令属于整数向量(ivec)域。例如,不存在布尔指令的特殊浮点版本。POR,ORPS与ORPD指令都相同。例如:

; Example 18.2a. Data transport delays on Bulldozer

movaps xmm0, [mem1]                        ; 6 clock

mulps xmm0, xmm1                              ; 6+1 clock

xorps xmm0, xmm2                               ; 2+1 clock

addps xmm0, xmm3                              ; 6+1 clock

movaps [mem2], xmm0                        ; 5 clock

                                                                   ; 28 clock total

这可以通过重排指令,使域之间的传输数减少(这里xorps指令用于改变符号,使得这个重排被允许),得到改进。

; Example 18.2b. Data transport delays on Bulldozer, improved

movaps xmm0, [mem1]                         ; 6 clock

xorps xmm0, xmm2                                ; 2+1 clock

mulps xmm0, xmm1                               ; 6-1 clock

addps xmm0, xmm3                               ; 6+1 clock

movaps [mem2], xmm0                         ; 5 clock

                                                                    ; 26 clock total

整数单元与浮点/向量单元间的传输时延,在我的测量中,比AMD的软件优化指引中说明的,要高得多。不过,我不能确定,就像指引建议的,通过一个内存立即数,从一个通用寄存器移动数据到一个向量寄存器会更快。

在一个浮点计算的输出是一个不同精度浮点计算的输入时,有大的惩罚,例如一个双精度浮点加法的输出是一个单精度加法的输入。这几乎没有现实的重要性,因为这样一个序列很可能就是一个编程错误,但它说明处理器在XMM寄存器的128位以外保存关于浮点值的额外信息。这个影响没有在Intel处理器上看到。

18.12. 不使用执行单元的指令

无需将NOP,FNOP与FWAIT指令发送到任何执行单元来解决它们。它们有每时钟周期4条指令的吞吐率。

128位寄存器到寄存器移动被实现为无需发送到任何执行单元的寄存器重命名。因此,它们有零时延,以及每时钟周期4条指令的吞吐率。在一个时钟周期里,同一个寄存器甚至可以被移动/重命名4次。指令MOVDQA,MOVDQU,MOVAPS,MOVUPS,MOVAPD与MOVUPA,在使用寄存器操作数时,都是相同的。

256位寄存器到寄存器移动是不同的。YMM寄存器的低半部以与128位寄存器移动相同的方式重命名,具有零时延,但高半部由P2或P3流水线中的一个执行单元移动,具有时延2。除了这个时延,在浮点代码中可能有传输时延,因为这个移动在整数向量域执行(参考表18.4)。例子:

; Example 18.3a. YMM move with transport delays on Bulldozer

vaddps ymm0, ymm0, ymm1               ; 6 clock

vmovaps ymm2, ymm0                         ; 2+2 clock

vmulps ymm2, ymm2, ymm3               ; 6 clock

                                                                   ; 16 clock total

这里,可以通过利用非破坏性3操作数指令,消除移动,节省5个时钟周期:

; Example 18.3b. YMM move eliminated

vaddps ymm0, ymm0, ymm1               ; 6-1 clock

vmulps ymm2, ymm0, ymm3               ; 6 clock

                                                                   ; 11 clock total

X87浮点指令FINSTP,FDECSTP与FFREE也是通过重命名,无需执行单元来解决的。重命名仅部分解决FXCH:它有零时延,但使用在P0或P1流水线中的一个执行单元。

其他所有寄存器移动使用执行单元,包括通用寄存器移动。

18.13. 寄存器的局部访问

处理器总是将整数寄存器的不同部分保持在一起。例如,AL与AH不被乱序执行不视为无关。因此,写一个寄存器一部分的指令,对之前该寄存器或寄存器任一部分的写,将有一个假依赖。

一个32位寄存器的写指令没有对相应64位寄存器的假依赖,因为该64位寄存器高半部置零。

写XMM寄存器一部分对整个寄存器有一个假依赖,但这不适用于YMM寄存器的两个半部。256位YMM寄存器被处理为两个无关的128位XMM寄存器。不过,这很少有实际后果,因为指令VEXTRACTF128与VINSERTF128被定序器处理为仿佛它们读/写寄存器的这两个部分。

处理器将算术标记的部分处理为无关的。例如,一条仅修改进位标记的指令没有对零标记的假依赖。

18.14. 打破依赖的指令

将一个寄存器置零的常见方法是XOR EAX, EAX或SUB EBX, EBX。处理器知道某些指令与该寄存器之前的值无关,如果两个输入寄存器是相同。以下指令被识别为与输入无关,如果两个操作数都是相同的寄存器:XOR,SUB,SBB(仅依赖进位标记),CMP,PXOR,PANDN,PSUBX,PCMPEQB,PCMPEQW,PECMPEQD,PCMPGTB,PCMPGTW,PCMPGTD,XORPS,XORPD,ANDNPS,ANDNPD。

对8位及16位寄存器,由于对寄存器局部的处理,这不奏效,但对32位或更大的寄存器,这是工作的。

在Steamroller上,指令PCMPEQQ,PCMPGTQ被视为无关,但在Bulldozer与Piledriver上不会。

浮点减法与比较从不视为无关,因为它们必须处理像NAN及INF的特殊情形。

仅当立即数是6(置零)或7(置全1)时,XOP指令VPCOMB,VPCOMW,VPCOMD,VPCOMQ被视为与输入无关。

在两个操作数不相同时,清零指令都使用相同的执行单元

18.15. 分支与循环

分支预测机制在第26页描述。不再有每16字节代码可以高效预测的分支数的限制。因为长的流水线,误预测的惩罚相当高。

小循环的速度受指令获取的限制。在Bulldozer与Piledriver上,小循环每迭代至少需要2时钟周期,如果它包含不超过一个被采用跳转以及没有32字节边界。更大的循环受限于指令获取速率或它们包含的指令。在Steamroller上,最多4条指令的微循环每迭代只需1时钟周期。

18.16. 缓存与内存访问

缓存

Bulldozer

Piledriver

Steamroller

1级代码

64 kB,2路,每行64 B,两核间共享。

64 kB,2路,每行64 B,两核间共享。

96 kB,3路,每行64 B,两核间共享。

1级数据

每核16 kB,4路,每行64 B。时延3 ~ 4时钟周期。

每核16 kB,4路,每行64 B。时延3 ~ 4时钟周期。

每核16 kB,4路,每行64 B。时延3 ~ 4时钟周期。

2级

1 ~ 2 MB,16路,每行64 B,两核间共享。时延21时钟周期。读吞吐率每4时钟周期1个。写吞吐率每12时钟周期一个。

2 MB,16路,每行64 B,两核间共享。时延20时钟周期。读吞吐率每4时钟周期1个。写吞吐率每12时钟周期一个。

2 MB,16路,每行64 B,两核间共享。时延19时钟周期。读吞吐率每4时钟周期1个。写吞吐率每6时钟周期一个。

3级

0 ~ 8 MB,64路,每行64 B,所有核间共享。时延87时钟周期。读吞吐率每15时钟周期1个。写吞吐率每21时钟周期一个。

0 ~ 8 MB,64路,每行64 B,所有核间共享。时延87时钟周期。读吞吐率每15时钟周期1个。写吞吐率每12时钟周期一个。

None

表18.5. AMD Bulldozer,Piledriver与Steamroller的缓存大小

数据缓存有两个128位端口可用于读或写。这意味着在同一时钟周期里可以进行两次读或者一次读与一次写。

在仅一个线程活动时,测得的吞吐率是每时钟周期两次读或者一次读与一次写。在多个线程活动上,我们不预期吞吐率会更低,因为每个核有独立的读写单元与1级数据缓存。但我的测量显示,在运行多个线程时,1级缓存吞吐率要慢几倍,即使线程运行在不共享任何1级或2级缓存的不同单元上。在Bulldozer,Piledriver与Steamroller上都观察到这个现象。没有找到对这个影响的解释。在相同单元运行的两个线程共享2级缓存吞吐率,但不受运行在不同单元的线程的影响。

非对齐读与写有每时钟周期一次读或写的吞吐率。在跨缓存行边界时,吞吐率是2或3时钟周期一次读或写,在跨内存页边界时,21时钟周期一次。

处理器可以在一个挂起的、不同地址的写之前进行一个读

在Bulldozer上,在基准测试中,出于未知原因,某些2级缓存有令人失望的、差劲性能。Piledriver与Steamroller有高效得多的缓存系统。Steamroller看起来比以前的模型有更多写缓冲。

1级数据缓存被组织为16个每个16字节的库。数据缓存在同一时钟周期里不能进行两个内存操作,如果它们使用相隔0x100倍数字节的库,除了来自相同缓存行的两个读。这种缓存库冲突发生得非常频繁:

; Example 18.4. Cache bank conflicts

mov eax, [rsi]                                      ; Assume rsi is divisible by 100H

mov ebx, [rsi+200h]                           ; Cache bank conflict. Delayed 1 clock

mov eax, [rsi]                                       ; Assume rsi is divisible by 100H

mov ebx, [rsi+210h]                           ; Different cache bank, no conflict

mov eax, [rsi]                                       ; Assume rsi is divisible by 100H

mov ebx, [rsi+10h]                              ; Same cache line, no conflict

在一个内存读地址相隔之前一个写0x1000倍数字节时,有一个假依赖:

; Example 18.5. False memory dependence

mov [rsi],  eax

mov ebx, [rsi+2000h]                          ; False dependence on previous write

mov ecx, [rsi+2004h]                          ; No false dependence

18.17. 写转发暂停

从一个写转发数据到相同地址的一个后续读,Steamroller比之前的设计快,特别对向量寄存器。

紧跟着写,从相同地址读,如果读比写大,会有25 ~ 26时钟周期的惩罚,因为写到读转发机制在这个情形下不工作。例子:

; Example 18.6. AMD store forwarding

mov [esi], eax                                       ; Write 32 bits

mov bx, [esi]                                         ; Read 16 bits. No stall

movq mm0, [esi]                                  ; Read 64 bits. Stall

movq [esi], mm1                                  ; Write after read. No stall

在Bulldozer与Piledriver上,如果读不是从与写相同的地址开始,有类似的惩罚:

; Example 18.7. Bulldozer and Piledriver store forwarding stall

mov [esi], eax                                       ; Write 32 bits

mov bl, [esi]                                          ; Read part of data from same address. No stall

mov cl, [esi+1]                                      ; Read part of data from different address. Stall

在Steamroller上,局部读写入的地址,没有惩罚:

; Example 18.8. Steamroller store forwarding stall

movdqa [esi], xmm0                           ; Write 128 bits

mov eax, [esi+8]                                  ; Read part of the data. No stall

vmovaps [esi], ymm0                         ; Write 256 bits as 2*128 bits

vmovaps xmm1, [esi+8]                     ; Crossing between two 128 bit writes. Stall

18.18. 在AMD Bulldozer,Piledriver与Steamroller里的瓶颈

AMD Bulldozer大体上是之前微架构的一个重新设计。某些最重要的改进是:

  • 提供每时钟周期4条指令的最大吞吐率的4条流水线。
  • 具有高吞吐率的、改进的浮点单元。
  • 将宏操作更好地调度到第一个空闲的执行单元。
  • 某些寄存器到寄存器移动被翻译为寄存器重命名。
  • 分支预测不再与代码缓存绑定,没有每代码缓存行分支数的限制。
  • 具有非破坏性3操作数指令的AVX指令集。
  • 高效的融合乘加指令。

Piledriver与Steamroller类似,具有某些改进。各种可能的瓶颈在以下章节讨论。

节能

节能特性在大多数时间里降低时钟频率。因为时钟频率变化,通常在性能测试中给出不一致的结果。要测量最大性能,有时必须关闭节能特性,或者在测试代码前放入一长串CPU密集代码。

共享资源

指令获取在构成一个计算单元的两个核间共享。分支预测器与浮点单元也是共享的。在Bulldozer与Piledriver上,指令解码器是共享的,而Steamroller每线程有一个解码器。某些操作系统没有足够的、关于资源共享的信息,因此它们可能将两个线程放入同一个计算单元,而其他计算单元是空闲的。

指令获取

共享的指令获取单元每时钟周期,在单线程应用中最多可以获取约20字节,在多线程应用中16字节。在平均指令长度超过4字节,或者频繁的跳转在流水线中产生空泡时,这很可能是瓶颈。

指令解码

在Bulldozer与Piledriver上,共享的解码单元每时钟周期可以处理4条指令。它在两个线程间交替,使每个线程每两个时钟周期最多得到4条指令,或者平均每时钟周期2条指令。这是一个严重的瓶颈,因为流水线的余下部分每时钟可以最多处理4条指令。

对产生多个宏操作的指令,情况变得更坏。所有产生超过2个宏操作的指令使用微代码使用。微代码定序器阻塞解码器几个时钟周期,使得其他线程在这个时间暂停。

在Steamroller中,指令解码成为瓶颈的可能性更小,因为它每核有一个解码器,支持每线程每时钟周期4条指令的吞吐率。

乱序调度

在Bulldozer与Piledriver上,整数乱序调度器有40项,共享的浮点调度器可能有更多。这是在以前设计上的一个重要改进。根据传闻,Steamroller有更多项与物理寄存器,但这没有被独立证实。

执行单元

在4条流水线间,整数执行单元分布不均。其中2条流水线有所有的整数执行单元,而其他2条流水线仅用于内存读指令及地址生成(非LEA),在某些模式上还用于简单寄存器移动。这意味着处理器每时钟周期仅可以执行两条整数ALU指令,而之前的模型可以执行3条。对纯整数代码,这是一个严重的瓶颈。对整数代码,实际上可以通过在向量寄存器中执行一半指令,加倍单核吞吐率,即使每个向量仅使用一个元素。

浮点执行单元分布要好些,因此有3或4条流水线可用。最常用的单元都是成对的,包括浮点加法、乘法与除法,以及整数加法与布尔操作。所有单元的宽度都是128位。对128位向量,这给出了一个高的吞吐率,在许多情形下,同时服务两个线程可能足够了。所有的256位向量指令被分解为两个128位操作,因此使用256位向量通常没有好处。Bulldozer与Piledriver有4条流水线用于浮点与向量操作,而Steamroller仅有3条。

融合乘加指令非常高效。在执行一次加法或一次乘法的时间里,它们执行一次加法与一次乘法。这实际上倍增了具有相同加法与乘法数的浮点代码的吞吐率。Bulldozer的FMA4指令与Intel的FMA3指令不兼容,实际不是AMD的错,就像在我博客里讨论的。

256位内存写

在Piledriver上,256位内存写异常慢。事实上,如此慢,在Piledriver上最好完全不用256位寄存器。在Steamroller中,这个问题已经解决了。

混用不同时延操作

对比之前的处理器,混用不同时延操作导致的问题减少。

依赖链

浮点指令与整数向量指令的时延是相对长的。因此,应该避免长依赖链。访问寄存器的局部会导致对该寄存器余下部分的一个假依赖。

跳转与分支

跳转与分支有每2时钟周期一个被采用分支的吞吐率。如果在跳转目标后附近有32字节边界,吞吐率更低。分支预测尚可,即使对间接跳转。因为长流水线,分支误预测惩罚相当高。

内存与缓存访问

在Bulldozer上,2级缓存有相当差的性能,在Piledriver与Steamroller上要好得多。缓存库冲突非常频繁,通常无法避免。在我的一些测试中,缓存库冲突被证明是一个严重的瓶颈。在我们认为必须服务两个线程时,代码缓存仅有相当低的2或3路。

回收

没有证据显示,回收会是一个瓶颈。

18.19. 文献

Software Optimization Guide for AMD Family 15h Processors. AMD, January 2012.

David Kanter: AMD's Bulldozer Microarchitecture. Real World Technologies, 2010.

M. Golden, S. Arekapudi and J. Vinh: 40-Entry unified out-of-order scheduler and integer execution unit for the AMD Bulldozer x86–64 core. Solid-State Circuits Conference Digest of Technical Papers (ISSCC), 2011 IEEE International.

D. Weiss, et. al.: An 8MB level-3 cache in 32nm SOI with column-select aliasing. Solid-State Circuits Conference Digest of Technical Papers (ISSCC), 2011 IEEE International.