10.10. 部分寄存器访问
一个通用寄存器的不同部分可以保存在不同的临时寄存器中,以避免假的依赖性。在写一个寄存器的一部分,后跟读该寄存器更大一部分时,出现一个问题:
; Example 10.4. Partial register access
mov al, 1
mov ebx, eax
Haswell与Broadwell解决了这个问题,没有可见的性能损失。也许它进行了部分寄存器与完整寄存器的双重簿记。
如果修改了其中一个高8位寄存器(AH,BH,CH,DH),然后读该寄存器更大一部分,情形就不同了:
; Example 10.5. Partial register problem with AH
mov ah, 1
mov ebx, eax
这里,插入一个额外的μop,在MOV EBX, EAX指令前面,将AH与EAX余下部分组合为单个临时寄存器。这导致了一个时钟周期的额外时延。
当标记寄存器一部分被修改,随后读该寄存器更大一部分时,出现类似的情形。这些情形中的某些,像例子10.5那样,要求一个额外μop。
; Example 10.6. Partial flags access
inc eax ; modifies zero flag but not carry flag
jbe L1 ; reads both sero flag and carry flag
; Example 10.7. Partial register access
bt eax, 2 ; modifies carry flag but not zero flag
cmovbe eax, ebx ; reads both carry flag and zero flag
在这些情形里,处理器将插入一个额外的μop来聚合标记寄存器的这两个部分。
在像这样的情形中,你可能认为这是一个编程错误,或者是使用单条指令对两个不同条件进行检测的、深思熟虑的测试。
在一个偏移或旋转指令后读标记寄存器,没有惩罚或额外的μop。
在重排缓冲中,一个XMM寄存器不会被分解。因此,无需额外的μop,在写XMM寄存器一部分时,没有部分访问暂停。但写向量寄存器一部分时,有对该寄存器之前值的一个依赖。参考例子8.8,第103页。
YMM寄存器的两部分在VEX指令中不会被视为无关的,但当在VEX与非VEX模式间切换时,这两部分可以分开。如章节9.12所述,在混合VEX与非VEX代码时,有70时钟周期的惩罚。
10.11. 缓存与内存访问
缓存 |
Haswell与Broadwell |
μop缓存 |
每核1536个μop,8路,每行6 μop |
1级代码 |
每核32KB,8路,64组,每行64B,时延4 |
1级数据 |
每核32KB,8路,64组,每行64B,时延4 |
2级 |
每核256kB,8路,512组,每行64B,时延12 |
3级 |
每核2 – 45MB,12 – 16路,每行64B,时延32,共享 |
表10.2. Haswell与Broadwell上的缓存大小
每个核有一个缓存,除了最后一级缓存。在可以运行两个线程的核上,所有的缓存在这两个线程间共享。将来可能有更多的版本有别的3级缓存大小。某些版本有一个4级缓存。256位的读写总线(参见第127页),使得使用256位寄存器拷贝或置零大的内存块优势明显。仅当源与目标都32字节对齐时,REP MOVS指令才能发挥完全的能力。在其他所有情形里,最好调用使用256位寄存器的库函数。
在之前的处理器中,缓存库冲突现象是一个性能问题。这个问题现在已经消除了。在同一时钟周期里,总是可以进行两个缓存读,而不会导致缓存库冲突。
不过,具有相同组及偏移的内存地址之间的假依赖性问题仍然存在。不能同时读、写相隔4K字节倍数的地址:
; Example 10.8. False memory dependence
mov [rsi], eax
mov ebx, [rsi+1000H] ; False memory dependence
非对齐内存的访问几乎没有惩罚,除了使用多个缓存行的影响。
10.12. 写转发暂停
在某些条件下,处理器可以将一个内存写转发给相同地址的一个后续读。写转发工作在以下情形里:
如果内存块跨越了64字节的缓存行边界,出现一个2时钟周期的时延。如果所有数据都自然对齐,这可以避免。
在以下情形里,写转发失败:
相比成功的写转发,失败的写转发需要多10个时钟周期。在一个没有对齐到至少16字节的128或256位的写之后,惩罚会高得多——大约50个时钟周期。
10.13. 多线程
某些版本的Haswell与Broadwell可以在每个核中运行两个线程。这意味着每个线程仅得到一半的资源。
资源以与Sandy Bridge相同的方式(参考第120页),在同一个核里运行的两个线程间共享。一个小的差异是,两个线程间存在一个循环缓冲。
如果任何共享资源是性能的限制因素,每个核运行两个线程没有好处。有如此多的执行端口与执行单元,执行很少成为限制因素。如果代码旨在达成每时钟周期超过两条指令,或者如果缓存大小是一个限制因素,那么在每个核上运行两个线程没有优势。
在CPU中,没有办法给一个线程高出另一个线程的优先级。
10.14. Haswell与Broadwell中的瓶颈
指令获取速率仍然限制在每时钟周期16字节,对不能很好放入μop缓存的代码,这可能是一个瓶颈。
对最多包含大约1000条指令的循环,μop缓存是高效。
在CPU密集代码中充分利用μop缓存是重要的。如果平均指令长度超过4字节,适合μop缓存的循环与不适合的循环之间的性能差异会相对显著。
μop缓存有与Sandy Bridge一样的缺点,如第109页与121页所述。
执行端口与执行单元的能力相当高。大多数常见的整数指令有4个执行单元可选,大多数浮点与向量指令有2个执行单元可选。因此,如果代码没有长的依赖链,获得每时钟周期4条指令的吞吐率是现实的。
几乎所有的向量执行单元与数据通路有完整的256位宽度。这使得使用256位向量寄存器以及AVX或AVX2指令集,非常有优势。
新的AVX2收集(gather)指令能高效地把非连续数据收纳入向量中,以及高效地向量化基于表的查找函数。在Broadwell中收集指令比Haswell更高效。
融合乘加(FMA)指令有助于提升浮点代码的性能。
在大多数情形里,寄存器到寄存器的移动被消除为0时延。
浮点向量乘法与FMA操作的吞吐率是每时钟周期两个向量操作,但浮点向量加法的吞吐率仅是每时钟周期一个向量操作。有两个乘法/FMA单元,但仅有一个加法单元,这很奇怪,因为通常在浮点代码中,加法比乘法更常见。如果代码包含的加法比乘法或FMA指令多,浮点的性能将是次优的。在我看来,对典型的浮点代码,这不是最好的设计,但至少它使Intel能夸口每时钟周期32 FLOPS的浮点性能。
对主要包含浮点加法的代码,把加法替换为带有一个乘数1.0的FMA指令,可以事实上提升吞吐率。FMA指令有时延5与每时钟周期两条指令的吞吐率,这意味着可能需要10个累加器寄存器来获得最大吞吐率。
执行时延通常是低的。大多数整数ALU操作仅有1时钟周期的时延,即使对256位向量操作。浮点加法时延为3,在Broadwell上浮点乘法的时延是3,在Haswell上是5。在长的依赖链中,这些执行时延是关键的。
分支目标缓冲与分支预测器的结构未知,但至少预测率看起来不错。
被采用分支的吞吐率是每时钟周期一次跳转,或每2个时钟周期一次跳转,依赖于分支的密度。被预测不采用的分支有更高的、每时钟周期2次的吞吐率。因此,组织分支,使它们不被采用的可能性最大,是有好处的。
相较于之前的处理器,内存端口的宽度被加倍了。到1级缓存的最大吞吐率,现在是每时钟周期2个32字节读以及一个32字节写。这使得以每时钟周期32字节的速度拷贝内存块成为可能。2级缓存的吞吐率远逊于此。
在之前的处理器上,缓存库冲突是一个相当常见的性能问题。在Haswell与Broadwell上。这个问题被完全解决了。
大多数关键资源在线程间共享。这意味着在多线程应用中,瓶颈变得更为关键。
Intel: "Intel 64 and IA-32 Architectures Optimization Reference Manual". July 2013.
David Kanter: "Intel’s Haswell CPU Microarchitecture". November 2012. http://www.realworldtech.com/haswell-cpu/
Anand Lal Shimpi: "Intel's Haswell Architecture Analyzed: Building a New PC and a New Intel". October 2012. http://www.anandtech.com/show/6355/intels-haswell-architecture
Anna Filatova: "4th Generation of Core Microarchitecture: Intel Haswell". September 2012. http://www.xbitlabs.com/articles/cpu/display/haswell-uarch-idf.html