8.11. 寄存器的部分访问
Core2与Nehalem使用3种不同的方式来解决寄存器部分写。这3种不同的方式分别用于通用寄存器,标记寄存器及XMM寄存器。
通用寄存器的部分访问
一个通用寄存器的不同部分可以保存在不同的临时寄存器中,以消除假的依赖性。例如:
; Example 8.6. Partial registers
mov al, [esi]
inc ah
这里,第二条指令无需等待第一条指令结束,因为AL与AH可以使用不同的临时寄存器。当μop被回收时,AL与AH写回永久EAX寄存器各自的部分。在写入寄存器的一部分,紧跟着从整个寄存器读时,出现一个问题:
; Example 8.7. Partial register problem
mov al, 1
mov ebx, eax
通过插入额外的μop来合并寄存器的不同部分,解决这个问题。我假定这个额外的μop在ROB-读阶段产生。在上面的例子中,ROB-读将产生一个额外的μop,在MOV EBX, EAX指令前,将AL与EAX的余下部分合并为一个临时寄存器。在ROB-读阶段,这需要2-3个额外时钟周期,但少于没有这个机制的处理器上5-6时钟周期的部分寄存器暂停惩罚。
写回高8位寄存器AH,BH,CH,DH产生两个额外μop,而写回寄存器低8位或16位部分产生一个额外μop。例子参考第87页。
标记寄存器的部分访问暂停
不幸的,在Core2与Nehalem上不能产生额外的μop来防止标记寄存器上的暂停。因此,在一条修改了部分标记寄存器的指令后,读标记寄存器会有一个大约7个时钟周期的暂停。例子参考第89页。
XMM寄存器的部分访问
在重排缓冲中,XMM寄存器不会被分解。因此,在写入部分XMM寄存器时没有部分访问暂停,无需额外的μop。但这个写对寄存器之前的值有一个假的依赖。例如:
; Example 8.8. Partial access to XMM register
mulss xmm0, xmm1
movss [mem1], xmm0
movss xmm0, xmm2 ; has false dependence on previous value
addss xmm0, xmm3
带有寄存器操作数的MOVSS与MOVSD指令只写目标寄存器的一部分。在例子8.8中,MOVSS XMM0, XMM2指令对前面的MULSS指令有一个假的依赖,因为寄存器的低32位不能与寄存器未使用的高半部分开。这妨碍了乱序执行。在例子8.8中的假依赖可以通过MOVAPS XMM0, XMM2替换MOVSS XMM0, XMM2来消除。不要使用带有两个寄存器操作数的MOVSS与MOVSD指令,除非保留寄存器余下部分不变是必要的。
8.12. 写转发暂停
在特定条件下,处理器可以把一个内存写转发给后续一个对相同地址的读。在大多数非对齐或部分内存引用的情形中,与之前的处理器一样,这个写转发会失败(参考第75页),但特定的特殊情形允许部分内存操作数的写转发。一个失败的写转发将推迟后续读大约10个时钟周期。
如果一个内存写后跟一个对相同地址的读,在读操作数有相同大小且自然对齐时,写转发可以工作:
; Example 8.9. Successful store-to-load forwarding
mov dword ptr [esi], eax ; esi aligned by 4
mov ebx, dword ptr [esi] ; No stall
如果操作数小于16字节,且在45nm Core2上没有跨越64字节边界,在65nm Core2上没有跨越8字节边界,写转发可以作用在非对齐内存操作数上。在Nehalem上,写转发作用在所有情形的非对齐内存操作数上。
; Example 8.10. Failed store forward because of misalignment on Core2
mov dword ptr [esi-2], eax ; esi divisible by 64
mov ebx, dword ptr [esi-2] ; Stall because 64 B boundary crossed
如果读比前面写有更大的操作数,写转发将不能工作:
; Example 8.11. 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 mm0, qword ptr [esi] ; Read 8 bytes. Stall
如果读比写有更小的操作数,在相同的地址开始,且写操作数在45nm Core2上没有跨越64字节边界,在65nm Core2上没有跨越8字节边界,写转发是可能的。Nehalem没有边界跨越的限制。
; Example 8.12. Store forwarding to smaller read
mov dword ptr [esi], eax ; Write 4 bytes
mov bx, word ptr [esi] ; Successful store forwarding
mov cx, word ptr [esi+2] ; Stall because not same start address
有几个特殊的情形,向在不同地址开始的更小读进行写转发是可能的。这些特殊情形是:
- 一个8字节写可以后跟都是它一半的4字节读,如果读在65nm Core2上没有跨越8字节边界,在45nm Core2上没有跨越64字节边界。Nehalem没有边界跨越的限制。
- 一个16字节写可以后跟都是它一半的8字节读,以及/或者都是它四分之一的4字节读,如果写对齐在16字节边界。Nehalem没有边界跨越及对齐的限制。
; Example 8.13. Store forwarding in special case
movapd xmmword ptr [esi], xmm0 ; Write 16 bytes
fld qword ptr [esi] ; Read lower half. Success
fld qword ptr [esi+8] ; Read upper half. Success
mov eax, dword ptr [esi+12] ; Read last quarter. Success
mov ebx, dword ptr [esi+2] ; Not a quarter operand. Fail
检测写转发是否可能的机制不区分在缓存中有相同组值的不同的内存。在地址间隔4kb倍数时,会导致暂停或失败的假的写转发:
; Example 8.14. Bogus store forwarding stall
mov word ptr [esi], ax
mov ebx, dword ptr [esi+800h] ; No stall
mov ecx, dword ptr [esi+1000h] ; Bogus stall
在例子8.4中,在写ax后读ecx时有一个暂停,因为内存地址有相同的组值(距离是0x1000的倍数),并且如果地址相同,一个小的写之后的大的读将给出一个暂停。
8.13. 缓存与内存访问
缓存 |
Core 2 |
Nehalem |
1级代码 |
32 kB,8路,每行64字节,时延3,每核 |
32 kB,8路,每行64字节,时延4,每核 |
1级数据 |
32 kB,8路,每行64字节,时延3,每核 |
32 kB,8路,每行64字节,时延4,每核 |
2级 |
2、4或6 MB,16或24路,每行64字节,时延15,共享 |
256 kB,8路,每行64字节,时延11,每核 |
3级 |
无 |
8 MB,16路,每行64字节,时延38,共享 |
表.8.3. Core2与Nehalem的缓存大小
除了最后一级缓存,每个核有一个缓存。在可以运行两个线程的核上,所有的缓存都在线程间共享。很可能将来会有更多版本有不同的最后一级缓存大小。在1级与2级缓存间有一个256比特数据通道。
重排内存访问的能力据说被提升了,因此可以在前面预期地址不同的写之前,在地址确切清楚前,推测执行内存读。
对1级与2级缓存,数据预取器能够以不同的步长自动预取两个数据流。
缓存库的冲突
数据缓存中的每个64字节行被分为16字节一个的4个库。如果两个内存地址有相同的库号,即Core2上两个地址第4与5比特位相同,在同一个时钟周期进行内存读与内存写是不可能的。例如:
; Example 8.15. Core2 cache bank conflict
mov eax, [esi] ; Use bank 0, assuming esi is divisible by 40H
mov [esi+100H], ebx ; Use bank 0. Cache bank conflict
mov [esi+110H], ebx ; Use bank 1. No cache bank conflict
Nehalem没有这些冲突,但在具有相同组与偏移的内存地址之间,即距离是4kB的倍数,Core2与Nehalem都有假的依赖。
非对齐内存访问
在跨越缓存行边界(64字节)时,Core2对非对齐内存访问有惩罚。这个惩罚对非对齐读大约是12个时钟周期,对非对齐写大约是10个时钟周期。Nehalem对非对齐内存访问几乎没有惩罚。
8.14. 打破依赖链
将一个寄存器置零的常见方法是XOR EAX, EAX或SUB EBX, EBX。Core2与Nehalem处理器知道特定的指令与寄存器之前的值无关,如果源与目标寄存器是同一个。
这适用于所有以下的指令:XOR,SUB,PXOR,XORPS,XORPD,及PSUBxxx与PCMPxxx的各种变体,除了PCMPEQQ。
以下指令在源与目标相同时,不视为无关:SBB,CMP,PANDN,ANDNPS,ANDNPD。
浮点减法与比较指令,在源与目标相同时,不是真正无关的,因为NAN等的可能性。
这些指令对打破不必要的依赖是有用的,但仅在知道这个无关性的处理器上。
8.15. Nehalem里的多线程
线程同步原语,即LOCK XCHG指令,比之前的处理器要快得多。
Nehalem可以在四个核中的每个运行两个线程。这意味着每个线程仅得到一半的资源。资源在同一个核上运行的两个线程间以下面的方式共享:
缓存:所有的缓存资源在线程间完全共享。一个线程用得越多,另一个线程用得越少。
分支目标缓冲与分支历史模式表:在线程间完全共享。
指令获取与解码:指令获取器与解码器在两个线程间平均共享,每个线程隔一个时钟周期获得这些资源。
循环缓冲:每个线程一个。
寄存器重命名与寄存器读端口:这些被平均共享,每个线程隔一个时钟周期获得资源。一个线程里的寄存器读暂停不影响另一个线程中的寄存器读。
重排缓冲与保留站:完全共享。
执行端口与执行单元:这些完全共享。一个线程可以使用一个执行端口,与此同时另一个线程使用另一个端口。
读与写缓冲:这些完全共享。
永久寄存器文件:每个线程一个。
显然,如果存在任何是性能限制因素的共享资源,每个核运行两个线程没有好处。在许多情形中,执行资源超出一个线程所需。在大量时间花在缓存不命中及分支误预测的情形里,每个核运行两个线程,优势特别明显。不过,如果任何共享资源是瓶颈,每个核运行两个线程就没有优势。相反,每个线程可能运行得单个线程一半的速度还要慢,因为缓存与分支目标缓冲的逐出以及其他资源冲突。在CPU里没有办法给一个线程高于另一个的优先级。
8.16. Core2与Nehalem中的瓶颈
指令获取与预解码
在Core2与Nehalem的设计中,流水线的所有部分都得到改进,因此整体吞吐率显著提升。改进最少的部分是指令获取与预解码。这部分不是总能跟得上执行单元的速度。因此,指令获取与预解码很可能是CPU密集代码中的瓶颈。
避免长的指令,以优化指令获取与预解码是重要的。最优的平均指令长度是大约3个字节,这不可能得到。
在适合循环缓冲的循环中,指令获取与预解码不是瓶颈。因此,如果最里层循环足够小,能放入循环缓冲,或者可以被分解为多个更小的、可以放入循环缓冲的循环,程序的性能可以得到提升。
指令解码
每时钟周期,解码器可以处理4条指令,或者在宏操作融合的情形下5条。4个解码器中仅第一个可以处理产生多个μop的指令。因此,解码器的最小输出是每时钟周期2个μop,在所有指令都产生2个μop,因此仅能使用第一个解码器时。应该按4-1-1-1模式排列指令来优化解码器吞吐率。
幸好,因为改进的μop融合、栈引擎以及128位宽的总线与执行单元,大多数在之前的设计中产生多个μop的指令,在Core2与Nehalem上仅产生一个μop。在一个所有指令仅产生一个μop的指令流中,解码器每时钟周期将产生4个μop。这与流水线余下部分的吞吐率相配。因此,仅在某些指令产生两个μop指令时,解码器吞吐率才是关键的。
长度改变前缀在解码器中导致长的时延。应该不计代价避免这些前缀,除了在能放入循环缓冲的小循环里。在32位及64位模式中,避免带有16位立即数的指令。
寄存器读暂停
在许多情形下,永久寄存器文件的寄存器读端口数是不足够的。因此,寄存器读暂停很可能是一个瓶颈。
在代码中避免2或3个以上经常读、但很少写的寄存器,栈指针,栈框指针,this指针,及保存在寄存器中的循环不变表达式,是寄存器读暂停可能的贡献者。循环计数器以及其他在循环中修改的寄存器也可能助长寄存器读暂停,如果循环每迭代需要超过5个时钟周期。
执行端口与执行单元
执行端口与执行单元的容量相当高。许多μop可以在2到3个执行端口间选择,每个单元每时钟周期可以处理一整个128位向量操作。因此,相比之前的设计,执行端口的吞吐率成为瓶颈的可能性减小了。
如果代码产生许多去往同一个执行端口的μop,执行端口会是一个瓶颈。在包含许多内存访问的代码中,内存操作会是瓶颈,因为仅有一个内存读端口与一个内存写端口。
大多数执行单元有流水线化为每时钟周期一个μop的吞吐率。最重要的例外是除法与平方根。
执行时延与依赖链
在Core2与Nehalem上执行时延通常是低的。大多数整数ALU操作仅有一个时钟周期的时延,即使128位向量操作。整数单元与浮点单元间的数据移动有1-2时钟周期的额外时延。仅在长依赖链中,执行时延是重要的。长依赖链应该被避免。
部分寄存器访问
在写入寄存器的部分后,读整个寄存器是有惩罚的。使用MOVZX或MOVSX把8位或16位内存操作数读入32位寄存器,而不是使用寄存器更小的部分。
回收
在我所有的实验里,都没观察到μop的回收成为一个瓶颈。
分支预测
分支预测算法是良好的,特别对循环。间接跳转可以被预测。不过,Core2上的分支历史模式表太小,使得分支误预测相当常见。
用于具有循环行为分支的特殊分支目标缓冲仅有128项,对有许多关键循环的程序,它可能是一个瓶颈。
内存访问
缓存带宽、内存带宽及数据预取要显著好于之前的处理器。
当然,对内存密集应用,内存带宽仍然可能是一个瓶颈。
文献
"Intel 64 and IA-32 Architectures Optimization Reference Manual". Intel 2009.
Jeff Casazza: "First the Tick, Now the Tock: Intel® Microarchitecture (Nehalem)". White Paper. Intel 2009.
Ofri Wechsler: "Inside Intel Core Microarchitecture: Setting New Standards for Energy-Efficient Performance". White Paper. Intel 2006.
David Kanter: "Inside Nehalem: Intel's Future Processor and System". www.realworldtech.com, 2008.