PowerPC 指令优化
首先需要认识到PPC体系下的CPU种类繁多,对具体需要优化的环境需要详细了解。例如流水线的类型如何?以往习惯了x86的思维后,我们都以为主频越高越好,流水线越长越好。其实不然。越长的流水线,分支预测失误代价越大,单条指令通过的时间越长。因此如果单算执行一条指令的速度,流水线长20的P4 2.0GHz 速度还没有流水线长 10的赛扬 1.2GHz快,而且Intel仅仅为了增加并行处理部分指令的机会而增加流水线长度,同时又要保持无法并行时的处理速度,为此只有增加主频,带来功耗的上升,以及分支预测失误的昂贵代价。
CPU需要根据科学型还是商务型及多媒体型来采取不同的设计优化策略:比如科学型计算机多用小而密集的循环计算,因此普通的分支预测命中率高(90%以上),因为大部分跳转都是向上跳转的循环,而商务型却只有50%的命中(大部分无规律的逻辑),多媒体型不但计算密集,而且内存吞吐量大。不同应用的CPU设计有所不同,优化也不同。
PowerPC以AS/400为例,多为短流水线体系,分支预测失误的代价更少,且主频更低(功耗更小),采用更“聪明”的预测机制,大部分主频很低,但速度惊人。以上流水线设计的两派技术体系争斗了十多年,各有千秋,很多主频比Intel低很多的PowerPC的芯片,却表现出了更优越的性能,而市场上大部分人只喜欢盲目追求主频,这是一个误区。
1. 指令结对原则
在类PPC405/440的系统中,指令被分为下面三类的其中之一,类405/440系统能够在单一周期同时执行两条不属于同一种类的指令:
(1) 数据的加载与存储
(2) 任意义下处理:设置CR寄存器进行比较,分支,乘除SPR寄存器更新
(3) 其他种类操作:非SPR/CR寄存器更改,算术与逻辑
如果两条邻接的指令属于同一类别,那么第二条必须等待第一条处理完以后才能被调度,这样做就浪费了时钟周期;而如果邻接的两条指令属于上述不同类别且无结果依赖,那么两条指令能够被同时调度,这样做就能获得比较高的效率。
这与我们x86下优化的经验并部相同,在x86的流水线中只要无倚赖的代码基本都可以并行运行,比如我们可以并行处理若干无相关的加载或计算,从而在x86下达到较高的效率:
mov eax, [esi + 10] 三条无依赖加载能并行
mov ebx, [esi + 14]
mov ecx, [esi + 18]
add eax, edx 三条无依赖加法能并行
add ebx, edx
add ecx, edx
而这样在大多数类405/440的PPC下却是有问题的。整数计算属于同一类别,邻接的无依赖计算指令不能如现代x86体系中得到同时运行;载入指令也相同,而整数计算和加载混合却能很好的并行调度:
lwz r3, 0(r10) 此加载和下一条加法无依赖,且属不同类别
add r4, r5, r5 加法能和前一条加载并行运行
lwz r6, 4(r10)
add r7, r8, r8
lwz r9, 8(r10)
add r3, r4, r4
因此如果我们的潜意识里过分熟悉x86优化方式,进而在用C开发的时候也会体现出来的话,可以说,这样的C代码在PPC下很难发挥效果的,即便编译器能优化,也需要给编译器留有优化的余地。
2. 加载依赖原则
当数据从缓存被加载到某寄存器的时候,需要数个周期以后数据才能被其他指令所使用,一条使用到刚被加载数据的指令需在数据被加载后第三个周期才能被调度。故在数据被加载与被使用两条指令之间的数个周期内形成了一段非常有效的优化区间,我们用来放置其他一些指令。加载与处理命令之间能放置的指令数决定于这些指令的种类,决定于他们是否能够结对并行处理,最大能有五条指令的优化空间。
在加载与使用命令之间能够并行插入的指令数取决于这些指令的混合方式,最少我们也可以插入两条指令进行优化。参考下面的指令,stw和lbz两条处于n+1和n+2周期的指令不能被结对并行执行,因为他们属于同一类别的指令。
虽然大部分加载指令只有一个目标寄存器,但是需要注意一些带“更新”功能的加载指令,诸如lwzu它除了更新目标寄存器外,同时也会更新源寄存器,此时对源寄存器的使用也必须等待该指令被完成执行以后才行。
3. 指令依赖原则
同x86类似,有上下文依赖的指令不能同时被调度。在两条指令中,如果第一条指令更新的寄存器会被第二条指令用到,那么这两条指令不能被同时调度,利用这个特性我们将依赖关系的两条指令分开,并且插入至少一条指令完成优化。
比如我们在周期n用add r4,r5,r6更新了R4寄存器,那么就只能到周期n+1才能调度到使用R4寄存器的srawi r7,r4,4指令。在第一条指令的第n周期,没有指令能够与之并行执行而造成了浪费,所有正确的方式是在这两条指令之间加入一条无相关的指令,这样便能和add指令进行结对而得到同时调度,充分利用了时钟周期。
4. 缓存优化原则
缓存优化的方法基本和x86相同,这里再对缓存优化的原理做一下说明,即处理器要使用主存某地址的数据时,需要先将他们加载到缓存,然后才能处理,最后更新回主存去。根据前面的加载依赖原理的阐述,我们知道从缓存加载到寄存器需要三个时钟周期后才可以使用该寄存器,然而如果该地址的数据不在缓存中的话,前面就需要加上更多周期的等待周期,让数据先加载到缓存,最终再经过三个周期的等待后才能使用该数据。
为了降低直接从外存到缓存昂贵代价,现代的处理器都增加了一条预取指令,在x86下叫做prefetch而在PowerPC中叫做DCBT(Data Cache Block Touch):
DCBT rA, rB - 将(rA+rB)所表示的地址数据预取到缓存
该指令将提前告诉CPU将用到哪块内存,CPU提前将该内存读入缓存,几个周期以后等到用时就该指令已经在缓存中了。用dcbt同x86的 prefetch指令,现代CPU的主要瓶颈在主存到缓存之间,高效使用缓存是优化的关键。
下面是一段x86下比memcpy快1.6倍的内存拷贝代码,原因在于对缓存的使用上,先mm0-mm7顺序加载,再顺序写入,读入到mm0与从mm0写入中间间隔7条指令,让CPU有足够的时间加载,同时使用了预取。
loop:
prefetchnta [esi + 256] 预取 esi + 256地址的数据
movq mm0, [esi + 0] 加载 esi + 0 到 mm0
movq mm1, [esi + 8]
…
movq mm7, [esi + 56]
movntq [edi + 0], mm0 写入mm0到 edi + 0
movntq [edi + 8], mm1 使用穿透缓存方式写入
…
movntq [edi + 56], mm7
add esi, 64 指针后移 64字节
add edi, 64 指针后移 64字节
dec ecx
jnz loop 判断计数器并循环跳转
而如果在PowerPC下写内存拷贝,我们就不能并列写若干加载指令,因为大部分PPC不能并行处理加载,我们需要将加载与存储交叉写:
loop:
dcbt r12, r11 预先加载 (r12+r11) 处内存到缓存
lwzu r3, 4(r11) 加载内存到r3并且移动指针
lwzu r4, 4(r11)
lwzu r5, 4(r11) 争取加载指令与写入指令并行运行
stwu r3, 4(r10) 写入数据从r3并且移动指针
lwzu r6, 4(r11)
stwu r4, 4(r10)
lwzu r7, 4(r11)
stwu r3, 4(r10) 利用多寄存器的特点写下去
lwzu r8, 4(r11)
….
addi r9, 0, –1 减少计数器
cmpwi cr4, r9, 0
blt loop 计数器未到就跳转
该段程序有三处优化,首先是缓存预取,dcbt在每个循环预先取后面的内容,其次是充分利用PPC多寄存器的特点,最后是让加载和保存指令交叉进行充分的发挥并行作用。
如果你所使用的PowerPC没有DCBT指令的支持,那么我们可以用一些小技巧来达到缓存预取的效果,即将DCBT指令替换成一条lwz来加载该地址数据到一个无用的寄存器,这种方法称为“硬预取”,在x86中也能可以使用该方法来起到缓存预取的作用。
5. PPC的AltiVec ™ 指令优化:
在PowerPC G4后开始支持AltiVec ™ ,这是一套类似x86下MMX+SSE的SIMD指令集,提供128位的矢量并行计算(8bit/16bit/32bit三种计算元)的功能,使多媒体计算平均提高4-5倍,而具体的AltiVec ™ 优化方法,超出本文叙述范围,读者可以自行查找相关资料。
6. 最终优化方法:
开启C编译器的汇编输出,在最大优化模式下思考编译器的优化策略。反复阅读对应 CPU的官方文档,试验、试验、再试验!最终您能写出漂亮高效的PPC代码。 |