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

9. Sandy Bridge与Ivy Bridge流水线

Intel的代号为Sandy Bridge的微架构是Core2与Nehalem设计的进一步发展。在解码器后添加了一个新的μop缓存,浮点执行单元从128位扩展到256位。

Sandy Bridge有2-8核,某些版本能够在每个核上运行两个线程。

支持新的AVX指令集。这对浮点向量操作,把16个128位XMM寄存器扩展为256位YMM寄存器。在AVX指令集中,大多数XMM与YMM指令有非破坏性的三参数版本。

9.1. 流水线

Sandy Bridge与Ivy Bridge流水线非常类似于Core2与Nehalem,但在解码器后添加了μop缓存。

在Sandy Bridge上重排缓冲有168项,在Haswell上有192项。在Sandy Bridge上保留站有54项,在Haswell上有60项。根据Intel的公开信息,Sandy Bridge有160个整数寄存器,144个向量寄存器;Haswell各有168个。

9.2. 指令获取与解码

预解码器与解码器每时钟周期可以处理16字节或4条指令,或者在宏操作融合的情形下,5条指令。它看起来与Core2与Nehalem中的解码器几乎完全相同。

带有任意数量前缀的指令在一个时钟周期里解码。

在某些情形下,长度改变前缀(参考第93页)的惩罚被消除了,仍有几个情形还有:

  • 使用一个操作数大小前缀,带有一个立即数的移动指令,即mov ax, 1234没有惩罚。
  • 使用一个操作数大小前缀,带有一个立即数的算术与逻辑指令,即add ax, 1234在解码器中有2-3个时钟周期的惩罚,不管是否对齐。这适用于32位或64位模式中,带有一个16位常量操作数的所有算术与逻辑指令。
  • 带有一个16位操作数的NEG,NOT,DIV,IDIV,MUL与IMUL指令没有惩罚。
  • 地址大小前缀不会导致惩罚,即使它改变了指令的长度。

有4个解码器,根据特定的模式,可以处理产生多个μop的指令。以下指令模式在我的实验中,可以在一个时钟周期内被成功解码:

  • 1-1-1-1
  • 2-1-1
  • 3
  • 4

产生3或4个μop的指令单独解码。生成超过4个μop的指令由效率较低的微代码处理。

在Sandy Bridge上,操作码0F 1F的多字节NOP指令仅能在四个解码器中的第一个里解码,而带有额外前缀(操作码66 66 90)的简单NOP可在任一解码器中处理。Ivy Bridge没有这个限制。在Ivy Bridge上,这两种长NOP以每时钟周期4个的速度解码。

9.3. μop缓存

Sandy Bridge与Ivy Bridge在解码器后有用于已解码μop的一个缓存。这是有用的,因为在获取/解码单元中每时钟周期16字节的限制是一个严重的瓶颈,如果瓶颈指令长度超过4字节。对能放入μop缓存的代码,吞吐率翻倍到每时钟周期32字节。

μop缓存组织为32组、8路、6个μop,最大容量1536个μop。对代码每个对齐、连续的32字节块,它最多能分配3行,每行6个μop。

耗尽μop缓存的代码不受获取及解码单元的限制。每时钟周期,它可以提交4个(可能融合的)μop,或等同的32字节代码。

μop缓存很少用到最大容量的1536个μop。由于以下原因,使用率通常少于最优:

  • 一个μop缓存行分配给一个特定的32字节代码块。每次经过32字节的代码时,将开始新的μop缓存行,即使前面的缓存行只是部分填充了。
  • 产生多个μop的指令不能分开在两个μop缓存行间。如果当前行不能完全包含一条指令,那么该行余下部分将不会使用,这条指令将被放在新行的开头。
  • 产生多于4个μop的指令使用微代码ROM。这样的指令使用一整个μop缓存行。
  • 无条件跳转或调用总是终止一个μop缓存行。
  • 在μop缓存中同一块代码可以有多个入口,如果它有多个跳转入口。
  • 要求超过32比特储存空间的指令在μop缓存中可能占据两项,需要一个额外的时钟周期载入。细节在下面给出。
  • 每时钟周期不能载入多个μop缓存行。如果许多指令使用两项,这可能是一个瓶颈。
  • 产生超过18个μop,或在μop缓存中要求超过18项的32字节代码块不会分配μop缓存。
  • 流水线在从解码器获取的μop与从μop缓存获取的μop之间频繁切换。每次切换需要1个时钟周期。

μop缓存中的每项有32比特储存空间用于地址与数据。一个μop可能需要超过32比特的储存空间,比如它同时有一个内存操作数与一个立即数。系统使用各种方法来处理这个问题。32位的储存空间可能分为两个16位域,一个用于地址比特,一个用于立即数比特。一个32位地址可以保存为一个16位符号扩展值,如果它在范围-215到215。类似的,一个32位立即数可以保存为一个16位符号扩展值,如果它在范围-215到215,一个64位立即数可以保存为一个32位符号扩展值,如果它在范围-231到231。不过,一个64位地址域不能转换为一个32位符号扩展值。

如果必须的数据不能压缩到一个32位值或16位值,那么如果可能,将从同一个μop缓存行的其他项借未使用的数据空间。如果这不可能,在μop缓存中将使用两项。

μop缓存行需要一个额外的时钟周期来载入,如果它有使用额外数据空间的项,不管这额外的储存空间是从另一个项借的,还是使用了额外的项,除了使用RIP相对取址的指令。

使用RIP相对取址的指令与其他指令的行为稍有不同。一个劣势是,32位存储空间不能分解为两个16位域。另一方面,它有如果使用了储存立即数的额外数据空间,无需额外时钟周期来载入的优势。

根据我的测试得到的细节,在下面的表9.1中给。

地址模式

地址或偏移比特

立即数比特

μop缓存项数

μop缓存载入时间

没有内存操作数

0

0, 8, 16, 32

1

1

没有内存操作数

0

64small

1

1

没有内存操作数

0

64

2

2

32位绝对地址

32small

0, 8, 16, 32small

1

1

32位绝对地址

32small

32

2

2

32位绝对地址

32

0

1

1

32位绝对地址

32

8, 16, 32

2

2

指针,索引或两者

0, 8, 16, 32small

0, 8, 16, 32small

1

1

指针,索引或两者

0, 8, 16, 32small

32

2

2

指针,索引或两者

32

0

1

1

指针,索引或两者

32

8, 16, 32

2

2

Rip相对

32small, 32

0

1

1

Rip相对

32small, 32

8, 16, 32

2

1

64位绝对地址

64small, 64

0

2

2

表9.1. μop缓存中不同指令的大小

注:32small表示一个范围在-215到215的32位值,64small表示一个范围在-231到231的64位值。

例子:

; Example 9.1. Instructions in μop cache

mov dword [rsi+4], 1000H                   ; one entry

mov dword [rsi+4], 10000H                 ; two entries

mov dword [rsi+40000H], 0                 ; two entries

mov dword [rsi+40000H], eax             ; one entry

cmp dword fs:[8], 2                               ; one entry

cmp dword fs:[8], 20000H                    ; two entries

cmp dword fs:[80000H], 2                    ; two entries

mov rax,-10000000H                             ; one entry (even if long form)

mov rax,-100000000H                           ; two entries

vinsertf128 ymm0,ymm1,[rip+x],1      ; two entries

由于μop缓存带来的性能增益相当可观,如果平均指令长度超过4字节。可以考虑以下优化μop缓存的方法:

  • 确保关键循环足够小,能放入μop缓存。
  • 将最重要的循环项与函数项对齐到32字节边界。
  • 避免不必要的循环展开。
  • 根据表9.1,避免有额外载入时间的指令。
  • 根据表9.1,要求额外数据空间的指令可与不使用数据空间的指令混用,使得有空闲的数据空间可借。
  • 如果一个关键循环的32字节代码块产生超过18个μop,或者不能放入μop缓存行,那么重新组织代码;或者使某些指令更长,如果这使得它能放入3个μop缓存行,可能是有用的。这避免了在μop缓存与解码器之间切换的代价。

9.4. 回路缓冲

Sandy Bridge与Ivy Bridge里保留了Nehalem里的28个μop的循环缓冲(参考第92页)。循环缓冲放在μop缓存之后,但在μop缓存不命中时,它也可以从解码器接收μop。循环缓存提升了由于各种原因在μop缓存里执行不良的小循环的性能。在μop缓存不是瓶颈的地方,事实上,这是大多数情形,循环缓冲没有可察觉的效果。

使循环小于28个μop,以利用循环缓存,是有益的。

​​​​​​​9.5. 微操作融合

μop融合与之前的处理器一样。某些在执行单元中需要2个μop的指令可以使用μop融合技术将这两个μop作为一个μop穿过流水线的大部分,以节省流水线带宽。参考第80页与94页。

通过查找手册4“指令表”,可以看到哪些指令使用μop融合。使用μop融合的指令在“非融合域”列出的μop数高于“融合域”列出的μop数。

​​​​​​​9.6. 宏操作融合

在比之前处理器更多的情形下,Sandy Bridge与Ivy Bridge可以将两条指令融合为一个μop(参考第95页)。

在特定情形下,解码器可以将一条算术或逻辑指令与一条后续的条件跳转指令融合为一个计算-跳转μop。在执行单元处,计算-跳转μop不分解,由执行端口5的分支单元作为一个μop执行。

CMP,ADD与SUB指令可与有符号及无符号分支指令融合。INC与DEC可与有符号分支指令融合,TEST与AND指令可与所有分支指令(包括无用的组合)融合,如下表所示:

第一条指令

可与这些融合(反之亦然)

不能融合

cmp

jz, jc, jb, ja, jl, jg

js, jp, jo

add, sub

jz, jc, jb, ja, jl, jg

js, jp, jo

adc, sbb

none

 

inc, dec

jz, jl, jg

jc, jb, ja, js, jp, jo

test

all

 

and

all

 

or, xor, not, neg

none

 

shift, rotate

none

 

表9.2. 指令融合

第一条指令可以有一个立即数或内存源操作数,但不能是两者。它不能有内存目标操作数。不能有RIP相对取址操作数。例子:

; Example 9.2. Instruction fusion

dec ecx

jnz L1                                        ; Fusion is possible

cmp dword ptr [esi], 0

je L2                                          ; No fusion. Both memory operand and immediate

dec dword ptr [esi]

jnz L3                                        ; No fusion. Memory destination operand

add eax, ebx

jo L4                                          ; No fusion. See table 9.2

cmp eax,[mem]                      ; Will use RIP-relative address in 64-bit mode

jg L5                                          ; Fusion only in 32 bit mode

JECXZ与LOOP指令不能融合。在Ivy Bridge上,指令融合甚至对跨越16字节边界的指令也能工作。不确定Sandy Bridge是否也适用。

如果在同一时钟周期,多个可融合指令对到达4个解码器,那么仅第一个对被宏融合。

程序员应该将可融合的算术指令与一条后续的条件跳转指令放在一起,而不是在它们之间调度其他指令;利用宏操作融合,最好在一个可融合对与下一个可融合对之间有至少3条其他指令。

指令融合可以增加吞吐率最大到每时钟周期5条指令。不幸的,它也会降低吞吐率。可融合算术/逻辑指令(ADD,SUB,INC,DEC,CMP,AND,TEST)的解码速度比相似不可融合指令(如OR)低。可能的解释是:如果任何可融合算术/逻辑指令进入最后一个解码器,解码将被推迟,该指令将在下一个时钟周期进入第一个解码器,以检查下一条是否为可融合的分支指令。这意味着,对这些不能放入μop缓存的代码指令,解码吞吐率降低,即使代码不包含分支。

9.7. 栈引擎

Sandy Bridge有一个专用的栈引擎,它与之前处理器上的工作方式相同,如第81页所述。

PUSH,POP,CALL及RET指令对栈指针的修改由一个特殊的栈引擎来完成,在流水线中,它紧跟在解码阶段之后,可能在μop缓存前面。这将流水线从修改栈指针μop的负担中解放出来。这个机制节省了栈指针的两次拷贝:一次在栈引擎中,另一次在寄存器文件与乱序核。这两个栈指针可能需要同步,如果PUSH,POP,CALL及RET指令序列后跟着一条直接读或修改栈指针的指令,比如ADD ESP, 4或MOV EAX, [ESP+8]。在需要两个栈指针同步的每个情形里,栈引擎插入一个额外的栈同步μop。更多细节参考第81页解释。

​​​​​​​9.8. 寄存器分配与重命名

所有整数、浮点、MMX、XMM、YMM、标记以及还可能段寄存器都可以重命名。浮点控制字也可以重命名。

寄存器重命名由显示在图6.1中的寄存器别名表(RAT)及重排缓冲(ROB)控制。来自解码器与栈引擎的μop通过一个队列去往RAT,然后到ROB-读及保留站。RAT每时钟周期可以处理4个μop。每时钟周期RAT可以重命名4个寄存器,甚至重命名同一个寄存器四次。

无关的特殊情形

将一个寄存器置零的一个常用方式是通过与自身XOR或减去自身,比如XOR EAX, EAX。处理器知道,如果两个操作数寄存器是相同的,特定的指令与寄存器之前的值无关。

这适用于以下所有指令:XOR,SUB,PXOR,XORPS,XORPD,VXORPS,VXORPD以及PSUBxxx与PCMPGTxx的各个版本,但不适用CMP,SBB,PANDN。

寄存器在重命名阶段由这些指令置零。这些置零指令的吞吐率是每时钟周期4条,因为不使用执行单元。

如果两个寄存器相同,PCMPEQxx指令将所有比特置1。这条指令被认为与寄存器之前的值无关,但它需要一个执行单元。因为与x87形式的浮点栈寄存器重叠,带有一个64位mmx寄存器的置零指令也使用一个执行单元。

无需执行单元的指令

上述寄存器由例如XOR EAX, EAX的指令置零的特殊情形,在寄存器重命名/分配阶段处理,无需使用任何执行单元。这使得这些置零指令及其高效,吞吐率为每时钟周期4条置零指令。进位标记可由CLC以同样高效的方式置零。

之前的处理器仅可以在寄存器重命名阶段处理FXCH指令。Sandy Bridge还可以在寄存器重命名/分配阶段处理这些特殊的置零指令,连同NOP指令。

因此,NOP指令,包括多字节NOP非常高效,吞吐率为4个NOP每时钟周期。出于效率的原因,使用多字节NOP比常用的伪NOP,比如MOV EAX, EAX或LEA RAX, [RAX+0],要好得多。

消除移动指令

Ivy Bridge(但Sandy Bridge不是)可以在寄存器分配阶段消除寄存器到寄存器的移动。下面的例子展示了这:

; Example 9.3. Move elimintaion

add eax,4

mov ebx,eax ; this move can be eliminated

sub ebx,ecx

在这个例子中,寄存器重命名可能会消除mov ebx, ebx指令。在第三条指令中代表ebx输入的物理寄存器就是代表第一条指令中eax输出值的寄存器。寄存器重命名在第4页解释。

移动消除不总是成功的。在必要的操作数未就绪时,它会失败。但在超过80%可能的情形里,通常能成功消除移动。串接的移动也可以消除。

所有32位与64位通用寄存器以及所有128位与256位寄存器的移动消除是可能的。

从8位寄存器到32位或64为寄存器的零扩展移动也可以消除,比如MOVZX EAX, BL。16位寄存器的零扩展移动不能消除。目标是8位寄存器、16位寄存器或mmx寄存器的移动不能消除。符号扩展移动不能消除。

到寄存器自身的移动不能消除,例如mov eax, eax是不能消除的。

一个被消除的移动有0时延,不使用任何执行端口。但消耗解码器带宽。

​​​​​​​9.9. 寄存器读暂停

自Pentium Pro起,寄存器读暂停是一个严重的、通常被忽视的瓶颈。所有基于P6微架构及其后继Pentium M、Core与Nehalem微架构的Intel处理器,有每时钟周期从永久寄存器文件2或3次读的限制。

在Sandy Bridge与Ivy Bridge中,这个瓶颈被最终消除。在我的实验里,我没找到寄存器读次数的实际限制。

​​​​​​​9.10. 执行单元

Sandy Bridge与Ivy Bridge有6个执行端口。端口0,1与5用于算术与逻辑操作(ALU)。在端口2与3上有两个相同的内存读端口,之前的处理器只有一个。端口4用于内存写。端口4上的内存写单元没有地址计算。所有写操作使用端口2或3计算地址。最大吞吐率是每端口、每时钟周期一个非融合μop。

端口0,1与5支持完整的256位向量操作。端口2,3与4对256位读写操作使用两个时钟周期。各单元在下面的表9.3与9.4中列出。

在通用寄存器与向量寄存器中的乘法有不同的端口。通用寄存器乘法器在端口1上,时延为3。整数与浮点向量乘法器在端口0上,对所有精度时延都是5。这两个乘法器可以同时运行,两者都完全流水线化,吞吐率为每时钟周期1个向量操作。

整数除法使用端口0上的浮点除法。这是仅有的没有流水线化的单元。

端口5上的跳转单元处理所有跳转与分支操作,包括宏融合的计算-分支操作。

对整数与浮点向量操作,处理器有不同的执行单元。例如,浮点向量移动(MOVAPS与MOVAPD)仅在端口5上执行,而整数向量移动(MOVDQA)可以在端口0,1或5上执行。

执行单元均匀分布在端口0,1与5之间。这使得每时钟周期执行3条向量指令成为可能,例如浮点向量乘法在端口0上。浮点向量加法在端口1上,浮点混排在端口5上。

执行端口

数据类型

操作

最大数据大小,比特

时延,时钟

0

gpivec

移动

128

1

1

gpivec

移动

128

1

5

gpivec

移动

128

1

0

gpivec

add

128

1

1

gp

add

64

1

5

gpivec

add

128

1

0

gpivec

Boolean

128

1

1

gpivec

Boolean

128

1

5

gpivec

Boolean

128

1

0

ivec

multiply

128

5

1

gp

multiply

64

3

0

gp

shift

64

1

1

ivec

shift

128

1

5

gp

shift

64

1

0

ivec

shuffle, pack

128

1

5

ivec

shuffle, pack

128

1

5

gp

jump

64

1

5

浮点

fp mov, shuffle

256

1

1

浮点

fp add

256

3

0

浮点

fp mul

256

5

0

浮点

fp divsqrt

128

10-22

5

浮点

fp boolean

256

1

2

所有

内存读

128

 

3

所有

内存读

128

 

4

所有

内存读

128

 

表9.3. Sandy Bridge里的执行端口

数据类型:gp = 通用寄存器,ivec = 整数向量

执行端口

数据类型

操作

最大数据大小,比特

时延,时钟

0

gpivec

移动

128

1

1

gpivec

移动

128

1

5

gpivec

移动

128

1

0

gp

add

64

1

1

gpivec

add

128

1

5

gpivec

add

128

1

0

gpivec

Boolean

128

1

1

gpivec

Boolean

128

1

5

gpivec

Boolean

128

1

0

ivec

multiply

128

5

1

gp

multiply

64

3

0

gpivec

shift

128

1

5

gp

shift

64

1

1

ivec

shuffle, pack

128

1

5

ivec

shuffle, pack

128

1

5

gp

jump

64

1

5

浮点

fp mov, shuffle

256

1

1

浮点

fp add

256

3

0

浮点

fp mul

256

5

0

浮点

fp div and sqrt

128

10-20

5

浮点

fp boolean

256

1

2

所有

内存读

128

 

3

所有

内存读

128

 

4

所有

内存写

128

 

表9.4. Ivy Bridge里的执行单元

数据类型:gp = 通用寄存器,ivec = 整数向量,浮点 = 浮点寄存器与浮点向量

整数向量操作的时延与通用寄存器里的操作相同。这使得在没有足够通用寄存器时,将MMX寄存器或XMM寄存器用于简单的整数操作是便利的。虽然支持向量操作的执行单元较少。

读写带宽

处理器有两个相同的内存读端口(端口2与3),之前的Intel处理器仅有一个读端口。端口2和3还用于计算地址。所有的内存写操作要求两个μop:一个用于在端口2或3上计算地址,一个用在端口4上执行写。

128位或更小的内存操作有每时钟周期两个读或一个写。因为仅有两个地址计算单元(端口2与3),每时钟周期不可能进行两次读及一个写。

对在新的YMM寄存器里的256位操作数,情形是不一样的。对一个256位读,每个读端口需要两个时钟周期(但仅一个μop),对256位写写端口需要两个时钟周期。显然,在一个256位读的第二个时钟周期里,读端口不能执行另一个读μop,但它可以在第二个时钟周期里执行一个用于写端口的地址计算μop。因此,每两个时钟周期获得两个256位读及一个256写是可能的。

当以最大内存带宽读写时,缓存库冲突的风险(参见下面)相当高。

数据旁路时延

与Nehalem相同的方式,如第100页说示,执行单元被分为域,并且对整数与浮点值有不同的寄存器文件。不过,在不同域或不同类型寄存器间传递数据的时延,在Sandy Bridge上比Nehalem上要小,通常是零。

像MOVD这样在通用寄存器与向量寄存器间移动数据的指令仅有一个时钟周期的时延,没有额外的旁路时延。类似的,从向量寄存器移动数据到整数标记寄存器的指令,像COMISS与PTEST没有额外的旁路时延。

在从一条浮点指令传递数据到一条整数混排指令时,通常有1个时钟周期的数据旁路时延,将数据传递回一条浮点指令还要多1个时钟周期,例如当在两条ADDPS指令间使用PUSHFD时。当在整数数据上使用一条浮点混合(blend)指令时,例如后跟BLENDPS的PADDD,也会发生同样的事情。在某些情形下,在使用错误类型的混排或布尔指令时,没有旁路时延。例如,我发现在混合PADDD与SHUFPS时没有时延。在使用错误类型的移动指令时,偶尔有旁路时延,例如替代MOVDQA的MOVAPS。

在错误的数据类型上使用读写指令,没有额外的旁路时延。例如,可以很方便地在整数数据上使用MOVHPS读或写一个XMM寄存器的高半部。

在YMM寄存器的高128位与低128位间移动数据的指令,总是有额外1个时钟周期的数据旁路时延。例如,VINSERTT128指令有两个时钟周期的时延,而其他不跨过128位边界的混排及混合指令有1时钟周期的时延。VINSERTF128指令的时延,与数据是否真的插入YMM寄存器的低半部或高半部无关。

混用不同时延的μop

在将不同时延的μop发布到同一个执行端口时,之前的处理器有一个回写冲突,如第101页所示。这个问题在Sandy Bridge上大体上解决了。执行时延被标准化,使得被发布到端口1的所有μop具有时延3,所有去往端口0的μop具有时延5。具有时延1的μop可以去往端口0,1或5。不允许其他时延,除了除法与平方根。

时延的标准化有避免回写冲突的好处。不好的地方是某些μop有不必要高的时延。

256位向量

在端口0,1及5上的执行单元有全256位吞吐率。看起来有两个128位数据总线,一个用于低128位,一个用于高128位。在高与低128位间移动数据有一个时钟周期的时延。一个256位寄存器的两部分不视为无关的,除了在非期望的“保存状态”中,如下面在章节9.12所解释。显然在“修改状态”中,两个半部总是同时传输的。

下溢与非正规数

在浮点操作接近下溢时,出现非正规数(denomral number)。在某些情形里,非正规数的处理代价非常高,因为非正规结果由微代码例外(microcode exception)处理。

在某些情形下,通过在硬件而不是在微代码例外中,处理下溢与非正规数,某些惩罚已经被消除。

在所有工作在正规值但给出非正规结果的操作中,处理器有大约140 – 150时钟周期的惩罚。对一个正规值与非正规值间的乘法有类似的惩罚,不管结果是否为正规值。一个正规值与非正规值相加没有惩罚,不管结果如何。对上溢、下溢、无穷及非值结果没有惩罚。

如果在MCXSR寄存器里同时设置“flush-to-zero”模式与“denormals-are-zero”模式,可以避免对非正规值的惩罚。

你可能感兴趣的:(Agner,Fog编写的优化手册)