高性能CPU的秘密——新技术篇

Author: Johan De Gelas(著)、双木(译) Date: 1999年 第21期 17版

    Rise MP6是X86 CPU中第一款具有真正双FPU流水线的CPU,那么,它的性能比PⅡ好吗?如果你读过有关Rise MP6的评测报告,你就知道,MP6的FPU性能同使用单条FPU流水线的Pentium MMX差不多。那么,K7的表现又将怎样呢?它的3条FPU流水线在运行实际程序时只能提高一丁点儿性能吗?
    很明显,设计一个能够在每个时钟周期中处理大量指令的CPU是提高CPU性能的最好方法。在上篇文章(见第18期17版)中我们看到,由于程序分支造成了在同一时钟周期中难以将许多指令同时发布到各执行单元,解决问题的方法是预测分支程序的执行情况。不过,除了程序分支外,还有下面3个主要原因使得X86 CPU不能在每个时钟周期中立即发布大量的指令:
    ·指令依赖性;
    ·X86架构只有8个通用寄存器;
    ·执行单元很忙,所需的执行单元未准备好。
    下面一个一个地解释。
#1    一、指令依赖性
    所谓“依赖性”就是指令的执行需要前个指令的运算结果。比如程序员经常使用的分支程序,而CPU设计者就很讨厌这种情况。看下面这个例子:
    A=C*2
    B=A+2
    只要变量A的值还不知道,B=A+2就不能进行运算。也就是说,只要指令1的结果没有写进寄存器,CPU调度器就不能把指令2发布到执行单元。在上篇文章中我们看到,程序分支会造成具有较长流水线CPU运行停滞的,解决方法是采用分支预测;这次我们又看到“依赖性”带来的问题,流水线越长,指令潜伏期也越长,等待前一指令运算结果的时间也越长,同样会造成CPU运行停滞,比较完美的解决方法就是采用“乱序执行”和使用更大容量的缓冲器。
#1    二、X86架构的8个通用寄存器
    有人大肆抨击Intel已满20岁的X86 ISA(Instruction Set Architecture,指令集架构)只有8个通用寄存器。他们用这个事实来证明X86 CPU(包括PⅢ、PⅡ、K7等等)是群“恐龙”,已不能同RISC CPU(如Motorola G3和G4,Sun UltraSPARC Ⅱ等)竞争。
    确实,只有8个通用寄存器可用是可笑的。大家都知道,寄存器只是用来暂时存放指令值的,如果CPU需要把两个值加起来,它需要用1个寄存器来存放运算结果,用2个寄存器来存放相加的数值。CPU不能直接对存放在主内存中的数值进行计算,而超标量CPU能并行处理大量的计算,8个寄存器完全不能满足要求,在同一时钟周期中,如果有3个指令发布,你总共需要3个输出寄存器和6个输入寄存器。我们该怎么办呢?
    聪明的Intel和NexGen的工程师们发现了突破这个限制的方法:“寄存器重命名”。采用寄存器重命名技术后就可以使用CPU内的秘密寄存器。PⅡ有40个那样的寄存器,K6有48个,而经典Pentium却没有。可以肯定,K7和Intel的P7也将拥有大量的寄存器,这样才能够提高每个时钟周期的平均指令执行率。
    寄存器重命名技术现在已经深深地扎根于超标量CPU中了。CPU是在解码过程中对寄存器进行重命名的,解码器把“秘密”的寄存器名字变为“通常”的寄存器名字,本质上是通过一个表格把X86寄存器重新映射到那些秘密寄存器。因此,尽管从外面看,现代X86 
    CPU只有8个通用寄存器,但实际使用到的寄存器远大于这个数目。虽然重命名和重映射技术似乎很复杂,但却保持了X86 CPU的竞争力。
#1    三、寄存器重命名和乱序执行技术的优点
    为了说明寄存器重命名技术和乱序执行技术带来的性能提高,我们模拟一个超标量CPU执行8个算术指令的例子。假设它在每个时钟周期中能对2个指令解码,引出计算结果是在指令发布后3个时钟周期发生的。在下表中,读者只需关注“发布”和“引出”列。
#1    1.按序执行
    (1)在第1个时钟周期,两个指令发布:它们互不关联,因此,它们将在3个时钟周期后(第4个时钟)引出;
    (2)在第2个时钟周期,我们首次遇到了“指令依赖”,指令3需要指令2的结果,此时指令3不能开始发布;
    (3)如果是按序执行,指令4、5、6就不能在指令3前发布。只有在第5个时钟周期时(指令2的结果已得到)才能发布指令3。
    (4)在第6个时钟周期有个大问题:我们想把结果写到寄存器R1,但这将改变指令5的结果。因此,我们只有在R1空闲时(第10个时钟周期)才能发布指令6。
    从^211701a^表中可以看到,尽管这个CPU是超标量的,每个时钟周期可以对2个指令解码,但它的性能是很差的。实际解码率(IPC,每个时钟周期的指令执行数)只有0.53。
#1    2.按序执行和寄存器重命名
    如果每次程序所需的寄存器正被使用,我们可以把数据放到另一个“秘密”的寄存器中。由于在第6个时钟周期将寄存器R1重命名,指令6和指令8不再耽误CPU的工作。结果是我们能够将IPC提高50%,寄存器重命名技术可以弥补X86处理器的缺点。
#1    3.乱序执行和寄存器重命名
    在按序执行中,一旦我们遇到指令依赖的情况,流水线就会停滞,如果采用乱序执行,我们就可以跳到下一个非依赖指令并发布它。这样,执行单元就可以总是处于工作状态,把时间浪费减到最少。乱序执行可以允许我们在发布指令3前发布指令4~8,而且这些指令的执行结果可以在指令3引出后立即引出(按序引出对X86 CPU来说是必需的),实际解码率又会增加25%。不过PⅡ和K6从乱序执行中得到的好处有限,因为如果CPU遇到指令依赖情况,它必须找到更多的非依赖指令进行发布。
    WinChip的性能表现让我们看到一个带有大容量一级Cache的按序执行CPU能够同乱序执行CPU竞争,在时钟周期方面,Cache未命中的代价是非常高昂的。带有大容量一级Cache的按序执行CPU性能,比只有较小容量Cache乱序执行CPU的性能要强。
    而Rise的工程师在这方面犯了错误,MP6的一级Cache只有16KB,因此Cache未命中的发生频率比其他CPU高,以致于它很难“喂饱”它的3条流水线。这是很可惜的事,因为一个按序执行CPU不是太复杂,因此可以做得更小。如果Rise CPU具有较大的一级Cache和高时钟频率,那么,对于像K6-2那样的乱序执行CPU来说,Rise CPU是一个凶狠的对手,它具有更好的浮点性能(双FPU流水线),而且成本也更便宜。集成256KB二级Cache的mP6-Ⅱ或许将纠正这个错误,但它要达到令人满意的时钟频率。
    由于K7采用大容量缓冲,因此它能及时发布足够多的非依赖性指令。大容量一级Cache、大容量缓冲和乱序执行,使K7的两条FPU流水线比Rise mP6的两条流水线更容易“喂饱”,效率更高。
#1    四、繁忙的执行单元
    为什么现代CPU中的执行单元比解码器数量多?看看实际程序代码,你会注意到在任何特定的时间段,一个确定的指令类型,如X87浮点指令、3DNow!或整数指令是具有支配权的。每个执行单元会专门执行确定的指令类型。因此,CPU设计者必须让解码器“喂饱”执行单元。
    K7的三个解码器是通用的,换句话说,大部分时间内每个时钟周期都有3条指令输出,所以,如果三个指令都是整数指令,K7就得准备3条整数执行单元,如果执行的都是MMX、3DNow!或X87指令,则还得有3条FPU流水线才能同解码单元匹配。明白了吗?K7要为每种类型的指令准备3个执行单元才能充分满足解码单元的要求。当然,K7只有在完美的条件下才能在每个时钟周期发布和执行3个指令。我们已经知道,由于指令的依赖性和程序分支的存在,CPU要并行地做大量工作是很困难的。
    注意,K7比K6-Ⅲ更合理,K6-Ⅲ有一个浮点单元和两个3DNow!/MMX单元,但它不能同时发布X87指令和3DNow!指令,因为这两种指令使用同样的寄存器。K7的多媒体单元则合并了3DNow!、X87和MMX单元。
#1    五、小结
    又老又旧的X86架构在今天仍然能同RISC竞争的主要原因是,现代的X86 CPU都具有先进的RISC核心、寄存器重命名、乱序执行等技术,从而突破了X86架构的限制。
    K7还不是一个“革命者”,它看来是目前进化得最和谐的乱序执行X86 CPU。Intel的Merced则采用了完全不同的全新设计,这块CPU会碾碎它的对手吗?