12. Intel Atom流水线
Intel Atom处理器有更为简单的设计,其中节能是最重要的目标。它几乎没有乱序执行能力。它有1到2个核,每个核可以运行两个线程,总计同时最多4个线程。缓存、解码器与执行单元都在两个线程间共享。它支持补充SSE3指令集。某些版本支持x64。
流水线有16级:3级用于指令获取,3级用于指令解码,2级用于指令分发,1级用于读寄存器操作数,1级用于执行,2级用于异常处理与多线程,1级用于提交结果。读-修改或读-修改-写类型指令作为一个μop处理。更复杂的指令分解为多个μop。
流水线每时钟周期可以处理两条指令。有两个执行端口,每个连接到一个整数单元与一个浮点/SIMD单元。
除了我自己的测试结果,这个描述基于第二手来源的有限信息(Scott Wasson: Intel's Atom processor unveiled. http://techreport.com/articles.x/14458/1, April 2008. Anand Lal Shimpi: Intel's Atom Architecture: The Journey Begins. http://www.anandtech.com/showdoc.aspx?i=3276&p=1, April 2008)。
12.1. 指令获取
在运行单线程时,指令获取速率平均大约是每时钟周期8字节。在少数情形下,比如所有指令都是8字节且8字节对齐,获取速率可以高达每时钟周期10.5字节,但在大多数情形里,在运行单线程时,获取速率稍低于每时钟周期8字节。当在一个核上运行两个线程时,获取速率更低。
在平均指令长度超过4字节时,指令获取速率可能是瓶颈。在执行因为其他某些原因暂停时,指令获取器可以赶上并填充指令队列。
12.2. 指令解码
两个指令解码器是相同的。带有最多3个前缀的指令可在一个时钟周期里解码。前缀超过3个的指令(这几乎不会出现,除非使用无用的前缀进行字节填充)有严重的时延。大多数指令仅产生一个μop。对长度改变前缀没有惩罚。
解码后的指令进入一个每线程16项的队列。如果一个线程被禁止,两个16项队列可以合并为一个32项队列。
12.3. 执行单元
有两个执行单元群(cluster):一个处理所有在通用寄存器上指令的整数群,及一个处理所有在浮点寄存器与SIMD向量寄存器上指令的浮点与SIMD群。一个内存访问群连接到整数单元群。在群之间移动数据是慢的。
来自解码器的μop可以被发布到两个执行端口,我称之为端口0与端口1。每个执行端口访问部分的整数处理群与浮点/SIMD处理群。我将称整数处理群的两部分为ALU0与ALU1,浮点/SIMD处理群的两部分为FP0与FP1。这样两个执行端口可以并行处理两个μop流,分工如下:
可由端口0及端口1处理的指令:
仅由端口0处理的指令:
仅由端口1处理的指令:
4个单元ALU0、ALU1、FP0与FP1可能都有一个整数ALU,虽然不能排除仅有两个整数ALU,分别在ALU0与FP0、ALU1与FP1间共享。在FP0里有一个乘法单元,一个除法单元。整数乘法与整数除法去往端口0。
SIMD整数加法器与偏移单元有128位的宽度及1时钟周期的时延。对单精度向量,浮点加法器有完整的128位能力,但对双精度仅有64位的能力。乘法器与除法器都是64位。
浮点加法器有5时钟周期时延,且完全流水线化,吞吐率是每时钟周期一个单精度向量加法。乘法器是部分流水线化的,时延为4时钟周期,吞吐率是每时钟周期一个单精度乘法。双精度与整数乘法有更长的时延及更低的吞吐率。从一个乘法开始的时刻到下一个乘法可以开始的时刻,从最理想情形的1时钟周期,到不那么理想情形的2个以上时钟周期。双精度向量乘法与某些整数乘法在时间上不能重叠。除法是慢的且没有流水线化。一个单精度标量浮点除法需要30时钟周期。双精度需要60时钟周期。一个64位整数除法需要207时钟周期。
手册4“指令表”中的指令列表显示了哪些指令使用哪些单元。
12.4. 指令成对
仅在指令被排序,使得同时可以执行两条时,得到每时钟周期两条指令的最大吞吐率。在遵守以下规则时,可以同时执行两条指令:
从这些规则知道同时进行内存读与内存写是不可能,因为它们都使用端口0下的内存单元。但同时进行浮点加法(没有内存操作数)及浮点乘法是可能的,因为它们分别使用FP1与FP0。
12.5. X87浮点指令
使用x87形式浮点寄存器的指令,由Intel Atom处理器以一个非常不幸的方式处理。一旦有两条连续的x87指令,这两条指令不能成对,相反因为解码器中的问题,导致额外的1时钟周期。这给出了每2个时钟周期仅一条指令的吞吐率,而使用XMM寄存器的类似代码有每时钟周期两条指令的最大吞吐率。
这适用于所有x87指令(名字以F开头),即使FNOP。例如,在我的测试中,一个100条连续FNOP的指令序列需要200个时钟周期。如果这100个FNOP被100个NOP分散,那么该序列仅需要100个时钟周期。因此,避免连续的x87指令很重要。如果在两条x87指令间没有东西可放,那么放入一个NOP。每两条指令一个NOP显然需要一半的带宽,但这仍然好于没有NOP时的四分之一带宽。
FXCH指令有1时钟周期的时延,而许多其他处理器对FXCH给出零时延。这是在Atom上运行x87形式浮点代码的深层缺点,因为浮点寄存器栈结构使这对使用许多FXCH指令是必要的。
因此,用使用XMM寄存器的SSE2形式的代码替换x87形式的浮点代码是有好处的。SSE2指令通常超过4字节,而x87指令要短些。以每时钟周期8字节的最大指令获取速度,我们可能使指令获取成为一个瓶颈,但x87指令更短的长度不足以抵消上述严重的缺点。
12.6. 指令时延
简单的整数指令有1时钟周期的时延。乘法、除法及浮点指令有更长的时延。
不像大多数其他处理器,当在同一个流水线里,混用不同时延的指令时,在Atom处理器中我没有发现任何时延。
LEA指令使用地址生成单元(AGU),而不是ALU。在依赖一个指针寄存器或索引寄存器时,因为AGU与ALU间的距离,这导致了4时钟周期的时延。因此,在大多数情形里,使用加法与偏移指令,比使用LEA指令更快。
在SIMD向量寄存器与通用寄存器或标记寄存器间移动数据的指令有4 ~ 5时钟周期的时延,因为整数执行群与浮点/SIMD群有独立的寄存器文件。
对目标以外类型使用XMM移动、混排与布尔指令,没有惩罚。例如,可以对浮点数据使用PSHUFD,对整数数据使用MOVAPS。
12.7. 内存访问
每个核有3个缓存:
每个缓存在两个线程间共享,但不能在核间共享。所有缓存都有硬件预取器。
带有内存操作数的指令,只要内存操作数也被缓存,与类似的带寄存器操作数的指令相比,无需额外的执行时间。但由于两个原因,使用内存操作数不是完全“免费的”。首先,内存操作数使用端口0下的内存单元,因此该指令不能与另一条要求端口0的指令成对。其次,内存操作数可能使指令代码更长,特别如果它有完整的4字节地址。当指令获取被限制在每时钟周期8字节时,这会是一个瓶颈。对运行在整数执行群的指令,缓存访问是快的,但对运行在浮点/SIMD群的指令是慢的,因为内存单元仅连接到整数群。使用浮点或XMM寄存器的指令通常需要4 ~ 5时钟周期来读写内存,而整数指令,由于写转发,仅需要1时钟周期的实际缓存时延,如下所述。依赖于一个最近修改的指针寄存器的内存读有3时钟周期的时延。
写转发非常高效。在某个时钟周期写入的内存操作数,可以在下一个时钟周期读回。不像大多数其他处理器,即使读操作数比前面的写操作数更长或有不同的对齐,Atom也可以进行写转发。我发现的仅有的写转发失败是在跨越缓存行边界时。
在跨越缓存行边界时,非对齐内存访问代价极高。跨64字节边界的非对齐内存读或写需要16个时钟周期。性能监控计数器显示,非对齐内存访问涉及1级缓存的4次访问,而两次访问就足够了。在不跨64字节边界时,非对齐内存访问没有代价。
12.8. 分支与循环
跳转与分支的吞吐率是每2时钟周期一个跳转。不采用的分支每时钟周期一个。因此,循环的最小执行时间是2时钟周期,如果该循环不包含16字节边界,而如果在循环里跨了16字节边界,则是3 ~ 4时钟周期。
分支预测使用一个12比特的全局历史寄存器,如第22页所述。这给出了相当好的预测,但分支目标缓冲(BTB)仅有28项。在我的一些测试中,BTB不命中率要高于命中率。分支误预测的代价最多是13个时钟周期,有时稍低。如果正确预测分支被采用,但因为BTB项被逐出,预测目标失败,那么惩罚大约是7时钟周期。这发生得非常频繁,因为模式历史表有4096项,而BTB仅有128项。
12.9. 多线程
每个处理器核可以运行两个线程。这两个线程竞争相同的资源,因此两个线程的速度比单独运行时的速度要慢。
缓存、解码器、端口与执行单元在同一个核上的两个线程间共享,而预取缓冲、指令队列与寄存器文件是分开的。
整个核的最大吞吐率仍然是每时钟周期两条指令,平均每个线程每时钟周期一条指令。
如果这两个线程需要相同的资源,例如内存访问,那么每个线程将在一半时间里得到竞争资源。换而言之,如果没有缓存不命中,每个线程每两个时钟周期访问内存一次。
有趣的是,我发现在运行两个线程时,每个线程的指令获取速度大于单线程的一半速度,但小于单线程的速度。在运行两个线程时每线程指令获取速度在每时钟周期4到8字节之间,但不会超过8。这个值严重依赖指令长度与对齐。这些发现显示,可能有两个指令获取器,在有限的程度上,在另一个线程不活动时,能服务同一个线程。
分支目标缓冲(BTB)与模式历史表在两个线程间共享。如果两个线程运行相同的代码(不同的数据),那么我们可能期望这两个线程在这两个表里共享相同的项。但是,这不会发生。这两个表显然由分支地址与线程号的某个简单的哈希函数索引,因此在这两个线程里相同的项不会使用相同的表索引。我的测试显示运行在同一个核上的两个线程,比单独运行的一个线程,分支误预测率稍高,而BTB不命中显著增多。在有许多分支的最坏情形里,每个线程的速度还不到单独运行线程的一半。
这些资源冲突仅适用于两个线程运行在同一个核上的情形。Atom处理器的某些版本有两个每个都能运行两个线程的核,同时最多运行4个线程。如果每个核仅运行一个线程,那么不存在资源冲突。幸运的是,大多数操作系统倾向于将两个线程放在两个不同的核上。但如果有超过两个线程,那么将有一些线程共享同一个处理器核。
向运行在同一个核里两个线程分配不同的优先级是不可能的。因此,一个低优先级线程可能从同一个核里运行的另一个线程得到资源,使高优先级线程很不幸地仅以最大速度的一半运行。
在我的测试中,这发生了几次。
12.10. Atom里的瓶颈
在Atom处理器里的某些执行单元相当强大。每时钟周期,它可以处理两个完整的128位整数向量ALU指令,虽然因为系统别处的瓶颈,这个能力很少被完全发挥。浮点加法单元也相当好,而乘法与除法比较慢。
在大多数情形里,执行很可能被执行单元以外的因素所限制。最可能的瓶颈有:
结论是,对CPU密集与内存密集应用,比如游戏、图形处理及浮点算术,Atom力有不逮。低的价格与功耗使它对要求不那么高的目的有用,如办公应用与嵌入式应用。运行4个线程的能力,使Atom对流量有限的服务器应用有用。