3.7 预取
最近的Intel处理器家族引入了几种预取机制来加速数据或代码的搬移并提升性能:
● 硬件指令预取器
● 为数据的软件预取
● 为数据或指令的Cache行的硬件预取
3.7.1 硬件指令预取与软件预取
在基于Intel NetBurst微架构的处理器中,硬件指令预取器一次读32个字节的指令,到64字节的指令流缓存中。Intel Core微架构的指令预取在2.2.2小结中讨论。
软件预取需要一个程序员使用PREFETCH暗示指令并预感某些合适的时序和Cache失败的位置。
在Intel Core微架构中,软件PREFETCH指令可以跨页边界来预取并可以执行遍历一到四个页。被发布的软件PREFETCH指令在页遍历完成并且DCU失败被探测到之后填充缓存分配隐退。软件PREFETCH指令可以以与普通的加载相同的方式触发所有硬件预取器。
软件PREFETCH操作以与从存储器的加载操作相同的方式工作,除了以下几个例外:
● 软件PREFETCH指令在对虚拟物理地址的翻译被完成之后隐退。
● 如果有一个异常,比如页错误,需要预取数据,那么软件预取指令不会预取数据而隐退。
3.7.2 先前架构的软件和硬件预取
在基于NetBurst微架构的奔腾4和Intel Xeon处理器除了软件预取外引入了硬件预取。硬件预取器对于从存储器取数据和指令流的操作是透明的,不需要程序员干涉。后续的架构一直提升并对硬件预取机制添加特性。硬件预取机制的更早的实现专注于从存储器预取数据和指令到L2;更近的实现提供了对从L2取数据到L1的额外特征。
在Intel NetBurst微架构中,硬件预取器可以追踪8条独立的流。
奔腾M处理器也提供了对数据的一个硬件预取器。它可以向前追踪12条独立的流以及向后4条流。处理器的PREFETCHNTA指令也将64个字节取到第一级数据Cache中,不过不污染第二级Cache。
Intel Core Solo与Intel Core Duo处理器提供了比起奔腾M更先进的数据硬件预取器。关键不同点在表2-23中列出。
尽管硬件预取器操作是透明的(不需要程序员干涉),不过如果程序员对数据访问模式做特定裁剪来适应其特征(它更适合小跨度的Cache失败模式),那么硬件预取器的操作最高效。优化数据访问模式来适应硬件预取器是高度推荐的,并应该比起使用软件预取指令来更优先考虑。
硬件预取器最好是用小跨度数据访问模式,不管是向前还是向后,一个Cache失败的跨度不超过64个字节。这对于数据访问来定位在发布加载操作的时刻是已知的还是未知的来说也是真实的。软件预取能补充硬件预取器,如果小心使用的话。
在硬件与软件预取之间要做个权衡。这适用于诸如有规律的访问以及跨度访问的应用特征。总线带宽、发布带宽(在关键路径上的加载延迟)以及访问模式是否适合非临时预取也将会具有影响。
要获得如何使用预取的具体描述,见第7章。
调整建议2:如果发现一个加载频繁失败,要么在它之前插入一个预取,要么(如果发布带宽需要关心)将此加载搬到执行更早的地方。
3.7.3 为第一级数据Cache的硬件预取
在Intel Core微架构中的L1硬件预取机制在2.2.4.2小节中讨论。一个类似的L1预取机制对带有CPUID签名为家族15和模型6的基于Intel NetBurst微架构的处理器中也可用。
例3-54描述了触发硬件预取器的一个技术。此代码演示了遍历一个链表并执行一些计算性工作,操作每个元素的2个成员,这两个成员驻留在2个不同的Cache行中。每个元素的大小为192字节。所有元素的总大小比适应于L2 Cache的要大。
例3-54:使用DCU硬件预取
; 原始代码 mov ebx, DWORD PTR [First] xor eax, eax scan_list: mov eax, [ebx + 4] mov ecx, 60 do_some_work_1: add eax, eax and eax, 6 sub ecx, 1 jnz do_some_work_1 mov eax, [ebx + 64] mov ecx, 30 do_some_work_2: add eax, eax and eax, 6 sub ecx, 1 jnz do_some_work_2 mov ebx, [ebx] test ebx, ebx jnz scan_list ; 有利于预取所修改后的序列 mov ebx, DWORD PTR [First] xor eax, eax scan_list: mov eax, [ebx + 4] mov eax, [ebx + 4] mov eax, [ebx + 4] mov ecx, 60 do_some_work1: add eax, eax and eax, 6 sub ecx, 1 jnz do_some_work_1 mov eax, [ebx + 64] mov ecx, 30 do_some_work_2: add eax, eax and eax, 6 sub ecx, 1 jnz do_some_work_2 mov ebx, [ebx] test ebx, ebx jnz scan_list
在被修改的代码序列中,从一个成员加载数据的额外的指令可以触发DCU硬件预取机制以在下一条Cache行中预取数据,允许操作第二个成员来更快完成。
软件可以在以下两种情况下从第一级数据Cache预取器获得增益:
● 如果数据不在第二级Cache,那么第一级数据Cache预取器允许对第二级Cache预取器的早触发。
● 如果数据在第二级Cache中而不在第一级Cache中,那么第一级数据Cache预取器将连续的Cache行的数据更早地带到第一级数据Cache中。
有一些情况软件应该注意不必要的DCU硬件预取触发而产生的潜在副作用。如果含有许多横跨许多Cache行的成员的一个大数据结构在被访问时,其成员只有一少部分真正被引用到,但有多个对访问同一条Cache行,那么DCU硬件预取器会触发不需要的取Cache行。在以下例子中,对“Pts”以及“AltPts”的引用将触发DCU预取来取额外的不需要的Cache行。如果探测到由于对代码部分的DCU硬件预取而产生性能上的很大负面影响,那么软件可以设法减少同时发生的工作组的大小小于L2 Cache的一半。
例3-55:避免引起DCU硬件预取来获取不必要的Cache行
while(CurCond != NULL) { MyATOM *a1 = CurrBond->At1; MyATOM *a2 = CurrBond->At2; if(a1->CurrStep <= a1->LastStep && a2->CurrStep <= a2->LastStep) { a1->CurrStep++; a2->CurrStep++; double ux = a1->Pts[0].x - a2->Pts[0].x; double uy = a1->Pts[0].y - a2->Pts[0].y; double uz = a1->Pts[0].z - a2->Pts[0].z; a1->AuxPts[0].x += ux; a1->AuxPts[0].y += uy; a1->AuxPts[0].z += uz; a2->AuxPts[0].x += ux; a2->AuxPts[0].y += uy; a2->AuxPts[0].z += uz; } CurrBond = CurrBond->Next; }
为了充分利用这些预取器,使用以下方式中的一种来组织和访问数据:
方法1:
● 将数据组织为连续的访问,通常可以在同一个4KB页内找到。
● 以常量跨度向前或向后访问数据,使得IP【译者注:指令指针】预取器向前或向后预取。
方法2:
● 以连续的Cache行组织数据
● 以递增寻址来访问数据,在连续的Cache中。
例3-56展示了对连续Cache行的访问,这可以从第一级Cache预取器中获得利益。
例3-56:使用L1硬件预取的技术
unsigned int *p1, j, a, b; for(j = 0; j < num; j += 16) { a = p1[j]; b = p1[j + 1]; // 使用这两个值 }
通过提升每次迭代的开始从存储器加载的操作,很可能从存储器到第二级Cache的Cache行传输对的延迟的一个重要部分将与传输到第一级Cache行并行进行。
IP预取器仅使用地址的最低8位来区分一个特定的地址。如果一个循环的代码尺寸大于256个字节,那么两次加载可能出现在最低8位相似而IP预取器将被约束。因而,如果你有一个超过256字节的循环,那么确保没有两个加载具有相同最低8位地址,为了使用IP预取器。
3.7.4 为第二级Cache的硬件预取
Intel Core微架构含有两个第二级Cache的预取器:
● Streamer——从存储器加载数据或指令到第二级Cache。要使用Streamer,得将数据或指令组织在128字节的块中,并且以128字节对齐。对这个块中的两条Cache行的其中之一的第一次访问,当它在存储器中时,触发Streamer预取Cache行对。对于软件,L2 Streamer的功能类似于在基于Intel NetBurst微架构中所发现的对毗邻的Cache行预取机制。
● 取数据逻辑(DPL)——DPL与L2 Streamer仅被写回存储器类型触发。它们只在页边界(4K字节)内预取。两个L2预取器都可以被软件预取指令和来自DCU预取器的预取请求触发。DPL也可以被为所有权的读(RFO)操作所触发。L2 Streamer也可以被由于L2 Cache失败而导致的DPL请求所触发。
软件可以从既根据指令指针又根据Cache行跨度来组织数据以获得增益。比如,对于矩阵计算,列可以被基于IP的预取所预取,而行可以通过DLP以及L2 Streamer来预取。
3.7.5 具有Cache能力的指令
SSE2提供了额外的具有Cache能力的指令,在SSE中提供那些扩展。新的具有Cache能力的指令包括:
● 新的流存储指令
● 新的Cache行冲刷指令
● 新的存储器栅栏指令
要获得更多信息请见第7章。
3.7.6 REP前缀与数据搬移
REP前缀通常与用于存储器相关的库函数,诸如MEMCPY(使用REP MOVSD)或MEMSET(使用REP STOS)的字符串搬移指令一起使用。这些带有REP前缀的STRING/MOV指令在MS-ROM实现,并具有几种带不同性能等级的实现变种。
实现的特定变种在执行时基于数据布局、对齐以及计数器(ECX)值来选择。比如,带有REP前缀的MOVSB/STOSB应该以计数器值来使用,计数器值要小于或等于三来获得最佳性能。
字符串MOVE/STORE指令具有多种数据粒度。为了有效的数据搬移,更大的数据粒度是偏优的。这意味着通过将一任意计数器值分解为一些双字加一单个字节,用一个小于等于3的计数值搬移,能获得更好的效率。
因为软件可以使用SIMD数据搬移指令来一次搬移16个字节,所以以下段落讨论了用于设计和实现诸如MEMCPY()、MEMSET()以及MEMMOVE()高性能库函数的通用准则。要考虑四个因素:
● 每次迭代的吞吐——如果两片代码具有大约相同的路径长度,效率偏向选择每次迭代搬移更大数据片的指令。同时,每次迭代的代码尺寸更少将一般来说减少负荷并提升吞吐。有时,这可能涉及一个迭代的循环结构的相对负荷与对迭代使用REP前缀的比较。
● 地址对齐——带有最高吞吐的数据搬移指令通常具有对齐限制,或者说如果目的地址与其自然数据尺寸对齐,那么操作效率会更高。尤其,16字节搬移需要确保目的地址在16字节边界处对齐,而8字节搬移最好能够让目的地址在8字节边界处对齐。在双字粒度上频繁地执行搬移用8字节对齐的地址性能更佳。
● REP字符串搬移 vs SIMD搬移——使用SIMD扩展的实现通用目的的存储器函数通常需要添加一些序言代码来确保SIMD指令的可用性,序言代码用于在运行时调整数据搬移的对齐。对于在考虑一个REP字符串实现与一个SIMD方法的比较中也要关注序言代码的负荷。
● Cache逐出——如果要被一个存储器例程所处理的数据量接近最后层硬膜上Cache的大小的一半,那么Cache的临时位置可能会遭受损失。使用流存储指令(比如:MOVNTQ,MOVNTDQ)能最小化冲刷Cache的效果。开始使用一个流存储的门限依赖于最后层Cache的大小。使用可判定的CPUID的参数枝叶来确定大小。
用于实现一个MEMSET()的流存储技术,类型库必须也要考虑那个应用能从该技术获利,只要它不需要立即引用那个目标地址。当在一个微基准配置上测试一个流存储实现,但在一个完整尺度的应用中违背时,这个假定很容易支撑。
当将通用探索法应用于通用目的、高性能库例程的设计时,当优化一个任意计数值N并且地址对齐时,下列准则会是有用的。不同技术对于最优性能会是必要的,依赖于N的量:
● 当N小于某个小量(该小量的门限对于不同的微架构而有所不同——经验上,当为Intel NetBurst微架构优化时,8可能是个不错的值)时,可以直接编码每种情况而不需要一个循环结构的负荷。例如,显式地使用两条MOVSD指令以及一条带有REP、计数值等于3的MOVSB可以处理11个字节。
● 当N不小但仍然小于某个门限值(该门限制对于不同微架构可能会有所不同,但可以通过经验来确定)时,使用运行时CPUID以及对齐序言的一条SIMD实现将可能传递更少的吞吐,由于序言的负荷。一个REP字符串实现应该偏向使用一个双字的REP字符串。为了提升地址对齐,使用MOVSB/STOB的一小片序言代码,且计数值小于4,可以被用于在开始使用MOVSD/STOSD之前剥离非对齐的数据搬移。
● 当N小于最后层Cache尺寸的一般时,吞吐考虑可能偏向以下两种情况的其中之一:
——使用一个REP字符串,具有最大数据粒度的一个方法,因为一个REP字符串对于循环迭代具有微乎其微的负荷,并且在序言/尾声代码中的分支预测失败负荷来处理地址对齐也可在许多迭代中分摊掉。
——使用具有最大数据粒度的指令的一个迭代方法,这里SIMD特征探测负荷、迭代负荷以及用于对齐控制的序言/尾声可以被最小化。在这两个方法之间的权衡可以依赖于微架构。
例3-57展示了在32位模式下使用STOSD对带有目的地址在双字边界处对齐的任一计数值所实现的MEMSET()的一个例子。
● 当N大于最后层Cache的一般尺寸时,使用16字节粒度的流存储,并含有地址对齐的序言和尾声,将可能更高效,如果目的地址将不被立即在后面所引用。
例3-57:带有任一计数尺寸以及4字节对齐目的的REP STOSD:
// memset()的一个C代码例子 void memset(void *dst, int c, size_t size) { char *d = (char*)dst; size_t i; for(i=0; i<size; i++) *d++ = (char)c; }
; 使用REP STOSD的等价实现 push edi movzx eax, byte ptr [esp + 12] mov ecx, eax shl ecx, 8 or ecx, eax mov ecx, eax shl ecx, 16 or eax, ecx mov edi, [esp + 8] ; 4字节对齐 mov ecx, [esp + 16] ; 字节计数 shr ecx, 2 ; 做双字 cmp ecx, 127 jle _main test edi, 4 jz _main stosd ; 剥离一个双字 dec ecx _main: ; 8字节对齐 rep stosd mov ecx, [esp + 16] and ecx, 13 ; 做count <= 3 rep stosb ; 用<=3最优 pop edi ret
由Intel编译器所生成的运行时库中的存储器例程对于地址对齐、计数值以及微架构有一个很广范围的优化。在大部分情况下,应用应该利用默认的Intel编译器所提供的默认存储器例程。
在某些情况下,数据的字节计数被上下文所知(正如相对于通过从一个调用的一个形参传递所知),并且可以采取一个比起需要一个通用目的库例程而言更简单的方法。例如,如果字节计数也小,那么使用小于四的一个计数的REP MOVSB/STOSB能确保良好的地址对齐以及循环展开来结束剩余的数据;使用MOVSD/STOSD能减少与此迭代相关联的负荷。
使用带有字符串搬移指令的一个REP前缀可以在上述所描述的情景中提供高性能。然而,使用一个带有字符串扫描指令(SCASB、SCASW、SCASD、SCASQ)或比较指令(CMPSB、CMPSW、SMPSD、SMPSQ)的REP前缀并不建议用于高性能。而是考虑使用SIMD指令。
3.7.7 增强的REP MOVSB与STOSB操作(ERMSB)
在基于代号名为Ivy Bridge的Intel微架构上处理器开始,使用MOVSB以及STOSB的REP字符串操作可以同时提供灵活性以及高性能的REP字符串操作,用于通常情况下的软件,像存储器拷贝和设置操作。提供增强的MOVSB/STOSB操作的处理器通过CPUID特征标志(EAX=7H, ECX=0H, EBX.ERMSB[bit 9] = 1)来枚举。
3.7.7.1 memcpy上的考虑
标准库函数memcpy的接口引入了与微架构相互影响的几个因素(比如源缓存与目的缓存的长度、对齐)来确定库函数实现的性能特征。实现memcpy的两个普遍方法从小代码尺寸vs最大吞吐来驱动。前者通常使用REP MOVSD+B(见3.7.6小节),而后者使用SIMD指令设置并需要处理额外的数据对齐限制。
对于支持增强的REP MOVSB/STOSB的处理器,实现带有REP MOVSB的memcpy将提供比起使用REP MOVSD+B的组合在代码尺寸上更紧凑的利益以及更好的吞吐。对于基于代号名为Ivy Bridge的Intel微架构上,使用ERMSB实现memcpy可能不能达到与256位或128位AVX替代方式相同的程度,这依赖于长度和对齐因素。
图3-4描述了一个第三代Intel Core处理器上使用ERMSB对REP MOVSD+B的memcpy实现的相对性能,当两个源和目的地址都在16字节边界处对齐并且源区域不与目标区域相互叠交。使用ERMSB总是给予比起REP MOSD+B更好的性能。如果长度是64的倍数,它甚至能产生更高的性能。比如,拷贝65-128字节花费40个周期,而拷贝128字节仅花费35个周期。
如果一个应用希望用其自己定制的实现来旁通标准的memcpy库实现,而且具有自由度来管理源和目的的缓存长度分配,那么值得控制其存储器拷贝操作的长度为64的倍数来利用代码尺寸以及ERMSB的性能利益。
使用一个SIMD寄存器来实现一个通用目的memcpy库函数的性能特征比起使用一个等价的通用目的寄存器而言更为出彩,依赖于长度,SSE2、128位AVX、256位AVX的指令集选择,相对的源/目的对齐,以及存储器地址对齐粒度/边界等等。
因而,在使用ERMSB与SIMD实现之间比较一个memcpy性能特征是高度依赖于特定SIMD实现的。本小节剩余部分讨论了使用ERMSB对未发布的、优化的128位AVX来实现memcpy相对性能,来描述代号名为Ivy Bridge的Intel微架构的硬件性能。
表3-3展示了使用增强的REP MOVSB对128位AVX,对于几种范围memcpy长度的memcpy函数实现的相对性能,源和目的地址都16字节对齐并且源区域与目的区域不叠交。对于memcpy长度小于128字节,使用ERMSB比所可能使用到128位AVX要更慢,由于在REP字符串中的内部启动负荷。
对于地址不对齐的情况,memcpy性能通常将相对于16字节对齐的情况减少(见表3-4)
3.7.7.2 memmove实现的考虑
当在源和目的区域之间有一个叠交,那么软件可能需要使用memmove而不是memcpy来确保正确性。在一个memmove()实现中可以与方向标志(DF)一起来使用REP MOVSB以处理源区域与目的区域开始部分叠交的后面部分的情况。然而,设置DF来迫使REP MOVSB从高到低地址拷贝字节将引起严重的性能下降。
当使用ERMSB来实现memmove函数时,可以探测上述情况并处理源区域中第一个后面的块,该块将作为目的区域的部分通过使用带有DF=0的REP MOVSB而被写到目的区域的非叠交部分。在后面部分的叠交块被拷贝之后,剩余的源区域可以也用DF=0正常处理。
3.7.7.3 memset实现的考虑
代码尺寸和吞吐的考虑也应用于memset()实现。对于支持ERMSB的处理器,使用REP STOSB将再一次给予更紧凑的代码尺寸以及比起在3.7.6小节中所描述的STOSD+B组合技术更好的性能。
当目的缓存是16字节对齐的情况下,使用ERMSB的memset()能比SIMD方式执行更高效。当目的缓存不对齐时,使用ERMSB的memset()性能相比于对齐情况会下降20%,对于基于代号名Ivy Bridge的Intel微架构上的处理器而言。相比之下,memset()的SIMD实现将会遭受更小的性能下降,当目的不对齐时。