四、前端部分:指令拾取和解码)

1、指令拾取包括分支预测)

在Nehalem的指令拾取单元instruction fetch unit)中包含有相关指令指针relative instruction point,RIP),每个线程状态thread context)各有一个。在指令拾取单元中还包含有分支预测器,用来预测下一条将被拾取的指令的RIP。对于分支预测器的很多细节,INTEL并没有公布,但它们是适合于工作在SMT模式的。并且Nehalem也将继续使用上一代的那些特殊的分支预测器,比如循环检测器loop detector),间接预测器indirect predictor)等。

当分支预测器选定一条分支时,分支目标缓冲branch target buffer,BTB)就负责预测目标地址。Nehalem使用了2级BTB结构。作为参考,K10使用了一个有2K 项entry)的BTB用于直接分支,和一个有512项的间接分支目标阵列indirect branch target array)。Nehalem的两级BTB设计,非常适用于有大量指令代码的任务,例如数据库、ERP和其它的商业应用,通过提高分支预测准确度而提升性能和能效。INTEL没有发布详细的内部结构。下面作出一些有依据的猜测。

有两种可能。一是两个BTB使用同样的预测算法,而一个存取更小一点的历史文件,包含最近时间内所使用的分支RIP和目标RIP。在这种情况下,它们的关系就象L1缓存和L2缓存一样因为分支目标也具有相当好的地址相关性)。举个例子,比如L1 BTB有256-512项,而更大的L2 BTB则有2K-8K项。如果分支RIP在L1 BTB中没有找到,然后就到L2 BTB中去找,以作出目标预测。

另一种可能RWT认为这种可能性比较小)则是两级BTB使用不同的预测算法和不同的历史文件。比如L1 BTB使用简单而快速的算法和相对较小的历史文件,而L2 BTB则使用更慢但更准确的算法,而且被配置成是具有优先权的预测器。如果L2 BTB不同意L1 BTB的预测,则它可以撤消override)L1 BTB的预测,去除掉流水线中错误拾取的指令,而从新预测的RIP处重新拾取指令。这种结构可能性不大,因为它的能效比较低下。对于这种预测器架构,通常的情况是L1和L2 BTB都独立的得出正确的分支目标——这就意味着在大多数的时间里,L2 BTB都是在浪费能源。而L1 BTB错误,L2 BTB是正确的情况,只占非常小的比例。

不过在更早一点的ANANDTECH的文章中有提到认为是第二种方式:通常会遇到这样的情况,L1 BTB作出预测是基于分支的类型,但实际上它并没有历史数据,这样准确度就很低。而L2 BTB有更多的历史数据,就可以提高准确度,而且L2 BTB可以在执行过程中就纠正L1 BTB所给出的)错误预测,而避免性能损失这就正是override的情况)。

现在不清楚哪一种观点更正确一些。

Nehalem另一个提升分支目标预测的机制是对返回堆栈缓冲return stack buffer,RSB)进行了重命名。当发生程序调用时,RSB就记录下地址,这样当结束调用返回时,就从该地址继续执行下去。但是当有很多程序递归调用时,RSB将会溢出,并且如果分支预测错误,那么也将会产生错误的返回地址。Nehalem通过重命名RSB,就避免了溢出。并且错误的分支预测也不会毁坏RSB,只要调用与返回地址是正确配对的,那么就可以从Nehalem的RSB中得到正确的数据。对于每一个线程都有一个专用的RSB,避免任何交叉弄脏数据。

拾取单元取得了每一个线程预测的下一个地址通常情况下也就是正确的下一个地址),就到ITLB指令旁路转换缓冲)和L1 I一级指令缓存)去标注。ILTB由每个线程静态分配平分),有128项缓存4KB的小页表,是4路联合方式的缓存;另外,每个线程还有7项用于缓存大页表文件2MB/4MB),是全联合方式。L1 I是32KB,4路联合方式,由线程竞争共享。进入指令缓存后,每周期16Byte128bit)的指令发送到预解码和拾取缓冲pre-decode and fetch buffer)。然后,每周期6条指令从预解码和拾取缓冲发送到有18个项目数的指令队列instruction queue)。在Core 2中,指令队列被用做循环缓存Loop Stream Detector,LSD,循环流检测器),这样碰上小循环指令数≤18)的时候,指令拾取单元就可以被关闭。而Nehalem的指令队列则只是作为指令被解码前的缓冲,因为它的循环缓存在流水线级数中,被放到了解码阶段的后面。

2、解码

当X86指令进入指令队列后,它们就等着被解码成微操作指令uop),这是一种类似于RISC的指令。Core 2和Nehalem都有4个解码器,一个复杂解码器和3个简单解码器。简单解码器能够处理可以解码成一个uop的X86指令,现在绝大多数SSE指令都是这种。复杂解码器能够处理解码成1-4个uop的X86指令,而比这还要复杂的指令,则由微代码序列器microcode sequencer)来处理。

Nehalem改进了宏操作融合macro-op fusion)。Core 2可以在32bit模式下,把TEST/CMP比较指令)和其后的Jcc条件分支指令)给解码融合成一个微操作uop):CMP+Jcc。这样就增加了解码带宽,并减少了微操作数量。而Nehalem的宏操作融合则包括了更多的条件分支指令:JL/JNGE, JGE/JNL, JLE/JNG, JG/JNLE。TEST/CMP和这些条件分支指令都将被解码成一个微操作:CMP+ Jcc。而且,Nehalem的宏操作融合可以是32bit和64bit模式。这是很重要的,因为现在大多数服务器都运行的是64bit操作系统,即使是桌面电脑也开始更多的欢迎64bit操作系统了。

一旦X86指令解码成微操作,就进入有28个项目数的微操作缓冲uop buffer),Nehalem将其作为前面介绍到的LSDLoop Stream Detector,循环流检测器)。如果一个循环不大于28个微操作,则Nehalem就可以将其缓存在LSD里,并发送到乱序执行引擎去执行,而关闭指令拾取单元和解码器。这样通过关闭解码器和使用更大的循环,就能够比Core 2的LSD节省更多的能耗。Nehalem的28项的微操作缓冲,能够保存相当于21-23条X86指令根据在一些游戏中的测试情况得出的估计)。X86指令转换为微操作的比率取决于工作量的不同,不过总的来说,Nehalem具有比Core 2更大一些的LSD缓冲。

比较有趣的是,Nehalem的LSD缓冲看起来更象是指令追踪缓存trace cache)。指令追踪缓存的目标是以动态程序的顺序来存储微操作,而不是象指令缓存里那样以静态编译顺序存储X86指令,因此可以从关键路径critical path,指需要时间最长的路径)中去掉解码器和分支预测器,而使得被堵塞的地方可以立刻取得指令。P4的指令追踪缓存的问题在于它太脆弱了,当其命中失败时,就必须一个接一个地重新)解码指令。通常指令缓存的命中率是在90%以上,而指令追踪缓存却远低于这个标准,很少时候超过80%,大多数时候是在50-60%。也就是说,在40-50%的时间里,P4都表现得象是一个单发射微处理器,而不能够完全利用上它的执行资源。而Nehalem的LSD缓冲取得了和指令追踪缓存几乎同样的目标,而且当它无法工作的时候即当循环太大时),也没有什么要命的性能损失。

在LSD缓冲之后,解码步骤的最后一步是专用堆栈引擎dedicated stack engine),除去所有被堆栈更改过的微操作。堆栈更改过的微操作都是被一个专用加法器给执行过了的,并写到前端的一个推测寄存器speculative delta register)。推测寄存器偶尔会和重命名结构的寄存器renamed architectural register)同步,而重命名结构寄存器中则保存有堆栈中的非推测值non-speculative value)。当堆栈处理过的微操作都被清除之后,剩下的微操作就进入乱序执行引擎去被重命名、发送、分配和执行。


相关内容