9.11. 寄存器的部分访问
一个通用寄存器的不同部分可以保存在不同的临时寄存器中,以消除伪依赖性。一个寄存器部分写后跟一个整个寄存器读时,出现一个问题:
; Example 9.4. Partial register problem
mov al, 1
mov ebx, eax
通过插入一个额外的μop来合并寄存器的不同部分,在Sandy Bridge中解决了这个问题。我假定这个额外的μop在ROB-读阶段生成。在上面的例子中,ROB-读将生成一个额外的μop,在MOV EBX, EAX指令前,将AL与EAX余下部分合并为一个临时寄存器。写一个部分寄存器没有惩罚,除非稍后读同一个寄存器更大的部分。
Ivy Bridge仅在高8位寄存器(AH,BH,CH,DH)被修改时插入一个额外的μop,在像例子9.4的情形里不会。
标记的部分访问暂停
Sandy Bridge与Ivy Bridge不仅对通用寄存器,还对标记寄存器使用一个额外μop来整合部分寄存器的方法,不像之前的处理器仅对通用寄存器使用这个方法。这发生在写标记寄存器一部分,后跟读该标记寄存器更大一部分时。因此,之前处理器的部分标记寄存器暂停(参考第89页)被一个额外的μop所替代。在一条旋转指令后读标记寄存器时,Sandy Bridge也产生一个额外的μop,Ivy Bridge不会。
向量寄存器的部分访问
在重排缓冲中,一个XMM寄存器不会分解为部分。因此,无需额外μop,在写一个XMM寄存器的部分时,也没有部分访问暂停。但写一个向量寄存器部分有对寄存器之前值的一个依赖。参考第103页的例子8.8。
在VEX指令里,YMM寄存器的两个半部总是视为相关的,但在VEX与非VEX模式间切换时,两个半部可以分开,如下所述。
9.12. VEX与非VEX模式间的转换
AVX指令集定义了三个处理器模式,如手册2“优化汇编例程”,章节13.6“使用AVX指令集与YMM寄存器”描述的那样。这三个状态是:
- (干净状态)YMM寄存器高半部没有使用,且已在为零。
- (修改状态)至少一个YMM寄存器的高半部被使用,且包含数据。
- (保存状态)所有YMM寄存器被分为两部分。低半部由XMM指令使用,高半部不变。所有高半部都保存在一个暂存器(scratchpad)中。如果转换到状态B需要,每个寄存器的两部分将被再次接合起来。
状态C是一个不期望的状态。在混合使用YMM寄存器的代码与使用XMM寄存器的非VEX指令代码时出现。BàC,CàB及CàA的转换,根据我的测量,在Sandy Bridge上,每个大约需要70个时钟周期。AàC及BàA的转换需要零或一个时钟周期。最好不混用VEX与非VEX代码,以及在任何使用VEX编码指令的代码序列后插入一条VZEROUPPER指令,避免至/自状态C的慢速转换。
9.13. 缓存与内存访问
缓存 | Sandy Bridge与Ivy Bridge |
μop缓存 | 每核1536 μop,8路,每行6 μop |
1级代码 | 每核32 kB,8路,每行64 B,时延4 |
1级数据 | 每核32 kB,8路,每行64 B,时延4 |
2级 | 每核256 kB,8路,每行64 B,时延11 |
3级 | 最多16 MB,12路,每行64 B,时延28,共享 |
表9.5. Sandy Bridge上的缓存大小
每个核上有一个缓存,除了最后一级缓存。在一个核可以运行两个线程时,所有的缓存在线程间共享。将来可能有更多版本具有不同的最后一级缓存大小。
缓存库冲突
在数据缓存中,每个连续的128字节,或两个缓存行,被分为8个库,每个16字节。如果两个内存地址有相同的库号码,即两个地址的4 – 6比特位相同,不可能在同一时钟周期里执行这两个读。例如:
; Example 9.5. Sandy bridge cache bank conflict
mov eax, [rsi] ; Use bank 0, assuming rsi is divisible by 40H
mov ebx, [rsi+100H] ; Use bank 0. Cache bank conflict
mov ecx, [rsi+110H] ; Use bank 1. No cache bank conflict
另外,在具有相同组与偏移的内存地址间,即相距4K字节的倍数,存在一个伪依赖:
; Example 9.6. Sandy bridge false memory dependence
mov [rsi], eax
mov ebx, [rsi+1000H] ; False memory dependence
未对齐的内存访问
对操作数大小是64位或更小的非对齐内存访问,几乎没有惩罚,除非使用多个缓存库。
指令预取
Ivy Bridge预取指令有一个问题。看起来Ivy Bridge浪费时间在预取已经在缓存中的数据。在Ivy Bridge上,从同一地址重复预取的测量吞吐率是每43时钟周期一次预取,而在Sandy Bridge上是每时钟周期两条预取指令的吞吐率。
9.14. 写转发暂停
在特定条件下,处理器可以将一个内存写转发给相同地址的一个后续读。相比之前的处理器,这个写转发可以在更多的情形下工作,包括非对齐的情形。写转发在以下情形工作:
- 一个64位或更小的写,后跟一个相同大小与相同地址的读时,不管是否对齐。
- 一个128或256位写,后跟一个相同大小与相同地址的读时,16字节边界对齐。
- 一个64位或更小的写,后跟一个更小的完全包含在写地址范围里时,不管是否对齐。
- 一个任意大小的对齐写,后跟两部分的两个读,或四部分的四个读等,在写地址范围里自然对齐时。
- 一个128位或256位对齐写,后跟一个64位或更小的、没有跨过8字节边界的读时。
对64位或更小的写转发,跨越缓存行边界没有惩罚。如果读的操作数比之前的写要大,或者与写地址部分重叠,写转发不能工作:
; Example 9.7. Failed store forwarding when read bigger than write
mov dword ptr [esi], eax ; Write lower 4 bytes
mov dword ptr [esi+4], edx ; Write upper 4 bytes
movq xmm0, qword ptr [esi] ; Read 8 bytes. Stall
在大多数情形里,对失败写转发的惩罚是大约12个时钟周期。
在写没有对齐到至少16字节边界时,对128位或256位写转发,惩罚会异常地大。在这个情形里,我测得在Ivy Bridge上,16字节读/写大约是50时钟周期,32字节读/写大约是210时钟周期。
9.15. 多线程
Sandy Bridge与Ivy Bridge的某些版本在每个核上可以运行两个线程。这意味着每个线程仅得到一半资源。资源以下面的方式在运行在同一个核上的两个线程间共享:
缓存:所有的缓存资源完全在线程间共享。一个线程用得越多,另一个可以用的越少。
分支目标缓冲与分支历史模式表:这些在线程间完全共享。
指令获取与解码:指令获取器与解码器在两个线程间平均共享,每个线程隔一个时钟周期使用它们。
循环缓冲:每个线程一个循环缓冲。
寄存器分配与重命名资源平均共享,每个线程隔一个时钟周期使用它们。
重排缓冲与保留站。这些完全共享。
执行端口与执行单元。这些完全共享。一个线程可以使用一个执行端口,而另一个线程使用另一个端口。
读写缓冲:这些完全共享。
永久寄存器文件:每线程一个。
显然,如果任一共享资源是性能的限制因素,每核运行两个线程没有好处。不过,在许多情形里,执行资源超出单个线程所需。在大量时间花在缓存不命中与分支误预测的情形里,每个核运行两个线程特别有优势。不过,任一共享资源是瓶颈时,每核运行两个线程没有优势。相反,每个线程很可能运行得比单个线程一半还慢,因为缓存与分支目标缓冲中的逐出,以及其他资源冲突。在CPU中没有办法给一个线程比另一个线程更高的优先级。
-
- Sandy Bridge与Ivy Bridge中的瓶颈
指令获取与预解码
指令获取与解码非常类似于之前的处理器,仍然是一个瓶颈。幸运的是,新的μop缓存降低了解码器的压力。
μop缓存
新的μop缓存有了显著的提高,因为在代码的关键部分适合μop缓存时,它消除了指令获取与解码的瓶颈。现在,很容易得到每时钟周期4条指令的最大吞吐率(或者宏融合的5条),即使对更长的指令。
程序员应该小心在CPU密集代码中经济地使用μop缓存。适合μop缓存的循环与不适合μop缓存的循环之间的性能差异相当可观,如果平均指令长度超过4字节。
在旧的P4/NetBurst处理器中,μop缓存有点类似追踪缓存(参考第39页),具有某些相同的弱点。地址与数据操作数需要超过32位储存的指令可能在μop缓存中使用额外的空间,且需要额外一个时钟周期载入。优化代码避免这些弱点是可能的,但这不可能由任何人来完成,而是最专业的汇编程序员。
对我而言,为什么处理器没有在代码缓存中标记指令边界是一个谜。这将消除指令长度解码的瓶颈,因而消除μop缓存的需要。AMD处理器这样做,旧的Pentium MMX也同样。
寄存器读暂停
这个旧的瓶颈,自Pentium Pro起困扰Intel处理器,最终被消除了。在我的测量中没有检测到这样的暂停。
执行端口与执行单元
执行端口与执行单元的能力相当高。许多μop可在2个或3个执行端口间选择,每个单元每时钟周期可处理一个完整的256位向量操作。因此,如果指令平均分配在端口之间,执行端口的吞吐率不是一个严重的瓶颈。
如果代码产生许多去往相同执行端口的μop,执行端口会是平均。
执行时延与依赖链
执行时延通常是低的。大多数整数ALU操作仅有一个时钟周期的时延,即使对256位向量操作。执行时延仅在长的依赖链中是关键的。
部分寄存器访问
在写寄存器一部分后,读整个寄存器有一个惩罚。使用MOVZX或MOVSX将8位或16位内存操作数读入一个32位寄存器,而不是使用寄存器更小的部分。
回收
在我的所有测试中,都没有观察到μop的回收成为一个瓶颈。
分支预测
分支预测器没有专门识别循环。带有许多分支循环的预测逊色于之前的处理器。分支历史模式表与分支缓冲可能比之前的处理器大,但误预测仍然常见。对适合μop缓存的代码,误预测惩罚要更小。
内存访问
Sandy Bridge有两个内存读端口,而之前的处理器仅有一个。这是一个重要的改进。在最大内存带宽利用率时,缓存库冲突相当常见。
Ivy Bridge预取指令有一个严重的问题,它极慢。
多线程
大多数关键资源在线程间共享。这意味着在多线程应用程序中,瓶颈变得更加关键。
文献
"Intel 64 and IA-32 Architectures Optimization Reference Manual". Intel 2011.