19. AMD Ryzen流水线
19.1. AMD Ryzen中的流水线
新的Zen微架构是AMD处理器的一个完全重新设计,而且相当成功。使用Zen架构的第一个处理器称为Ryzen。它有4到8个核,每个核可以运行两个线程。每个核的吞吐率是如此的高,它可以同时处理两个线程,而没有严重的瓶颈。
19.2. 指令获取
一个执行单元里的两个核共享指令获取器。根据AMD文档,指令获取器每时钟周期可以从1级代码缓存获取32字节对齐的代码字节,但最大测得吞吐率每时钟周期仅高于16字节,很少超过17。
19.3. 指令解码
指令边界没有在代码缓存中标记,不像之前的AMD处理器。解码器每时钟周期可以处理4条指令,包括产生两个μop的指令。流水线余下部分每时钟周期可以处理6个μop。
如果至少一半指令产生两个μop,可以获得每时钟周期6个μop的最大吞吐率。
产生超过两个μop的指令使用微代码。这些指令至少需要2时钟周期解码,因此吞吐率不会超过每2时钟周期一条复杂指令。每条指令产生的μop数,在手册4“指令表”中列出。
有许多前缀指令的解码,没有惩罚。这包括所有类型的前缀。
19.4. 指令融合
后面紧跟着一个条件跳转的一条CMP或TEST指令,可以融合为一个μop。这适用于所有版本的CMP与TEST指令以及所有的条件跳转,除非CMP或TEST指令有一个rip相对地址,或者同时有一个位移与立即数。参考第178页例子18.1。其他ALU指令不能与条件跳转融合。
融合分支指令,如果没有被采用,可以每时钟周期两个这样的分支的吞吐率执行,如果采用,每2时钟周期一个分支。
19.5. μop缓存
对已解码指令,处理器有一个额外缓存。大小显示为2048个μop,每行8个μop。对保存最关键的循环,这足够大了。
对能放入μop缓存的循环,测得的吞吐率是每时钟周期5条指令。仍然可以从μop缓存以这个吞吐率发布双指令,因此以每时钟周期6个μop吞吐率,执行μop是可能的,这受到μop队列的限制。如果两个μop是类似的,双指令可能仅在μop缓存中使用一个项。
19.6. μop队列
在寄存器重命名与调度器之前,有一个未知大小的队列。这个队列从μop缓存或直接从解码器接收已解码μop。
19.7. 栈引擎
处理器有一个重命名栈指针的高效栈引擎。它放在μop队列后面。
Push,pop与return指令仅使用一个μop。就栈指针而言,这些指令有零时延,使得后续依赖于栈指针、作为操作数或指针的指令不会被推迟。没有观察到在Intel处理器中看到的额外栈同步μop。
19.8. 寄存器重命名与乱序调度器
在μop队列后,μop被分到两个单元。一个通用寄存器上指令的整数单元处理,以及一个处理浮点与向量指令的浮点单元。每个这些单元有自己带有寄存器重命名、调度及几个执行单元的寄存器文件。
整数寄存器文件有168个64位物理寄存器。浮点寄存器文件有160个128位寄存器。
在行进中,调度器可以保存总共192个μop。在1时钟周期可以回收8个μop。
19.9. 整数执行流水线
整数单元有4个ALU,因此它可以每时钟周期执行4条整数指令。简单整数指令可由这4个ALU中的任一个处理,而某些代价更高的操作,比如乘法与除法,仅可由其中一个ALU处理。
除了4个ALU,整数单元有两个地址生成单元(AGU)。除了ALU,带有内存操作数的指令还使用一个AGU。读-修改与读-修改-写指令不分解为多个μop,但同一个μop同时去往ALU与AGU。在同一个时钟周期中,进行两次内存读,或者一次读与一次写,但不是两次写,是可能的。
19.10. 浮点执行流水线
有4个浮点/向量执行流水线。这些流水线每个包含用于以下目的的执行单元:
操作 | P0 | P1 | P2 | P3 |
整数加法 | x | x |
| x |
整数乘法 | x |
|
|
|
浮点加法 |
|
| x | x |
浮点乘法与FMA | x | x |
|
|
浮点除法与平方根 |
|
|
| x |
布尔逻辑 | x | x | x | x |
偏移 |
| x |
| x |
封装,排列 |
| x |
| x |
混合(Blend) | x | x |
| x |
加密 | x | x |
|
|
转换 |
|
|
| x |
内存写 |
|
| x |
|
表19.1. Ryzen浮点与向量执行流水线
简单的整数向量指令,比如加法、偏移与布尔指令,相比之前AMD处理器上的2时钟周期,仅有1时钟周期的时延。浮点加法有3时钟周期时延。对单精度,乘法有3时钟周期时延,对双精度有4时钟周期时延。融合乘加(FMA)有5时钟周期时延。吞吐率是每时钟周期两个128位加法与两个128位乘法。FMA指令使用与乘法相同的流水线,它们也部分占据加法单元。混合的FMA与加法指令的测得吞吐率是每时钟周期4条128位FMA指令与4条128位加法。
256位指令被分为两个μop,因此256位向量的吞吐率是128位向量吞吐率的一半。大多数指令可在两条或更多流水线间选择,如表19.1所示,因此大多数256位指令可以每时钟周期至少1条指令的吞吐率执行。
储存单元没有加倍。因此,256位写的吞吐率是每时钟周期一个。
不再支持XOP,TBM与3DNow指令。FMA4指令工作正确,虽然没有正式支持它们,且CPUID指令不报告它们。
次正规操作数
给出一个次正规结果的浮点操作需要额外几个时钟周期。在乘法或除法下溢到零时也一样。这远小于Bulldozer与Piledriver上高惩罚。在同时打开flush-to-zero模式与denormals-are-zero模式时,则没有惩罚。
19.11. AVX指令
Ryzen支持AVX2指令集。256位AVX与AVX2指令被分为两个每个处理128位的μop。大多数256位指令的吞吐率是每时钟周期1指令,因为有两个乘法单元与两个加法单元。256位指令以每时钟周期4条指令的速度解码。因此,在指令获取与解码是瓶颈时,使用256位指令比128位指令更高效。
在单线程中,浮点计算的最大吞吐率是每时钟周期一个256位向量乘法或FMA指令与一个256位向量加法。
在这个处理器上,混用AVX与非AVX向量指令没有惩罚。
19.12. 不同执行域间的数据时延
向量单元中执行单元分为两个域,一个整数向量域与一个浮点向量域。在整数向量域一个μop的输出用作浮点域一个μop的输入时,有额外1时钟周期的时延,反之亦然。
例如,如果一条ADDPS指令的输出用作一条POR指令的输入,有额外1时钟周期的时延。可以通过等效的ORPS指令替换POR指令来避免这个时延。大多数浮点指令位于浮点域,除混排等指令。
指令的域信息参考手册4“指令表”。
19.13. 没有时延的指令
寄存器到寄存器移动在寄存器重命名阶段解决,无需使用任何执行单元。这些指令有零时延。每时钟周期执行这样6条指令是可能的,甚至可以在一个时钟周期里重命名同一个寄存器7次。
在使用寄存器操作数时,以下指令有零时延:MOV,XCHG,FXCH,(V)MOVDQA,(V)MOVDQU,(V)MOVAPS,(V)MOVUPS,(V)MOVAPD与(V)MOVUPD。这适用于32位与64位通用寄存器以及128位xmm寄存器。它不适用8位与16位局部寄存器与mmx寄存器。使用256位ymm寄存器的移动有1时钟周期的时延,因为这个寄存器被分解为2个128位部分,在高半部使用一个执行单元进行移动时,仅低半部被重命名。
19.14. 寄存器的局部访问
处理器总是将一个整数寄存器的不同部分保持在一起。例如,乱序执行机制不把AL与AH视为无关。因此,写一个寄存器局部的指令将对相同寄存器或其任一部分的之前值写,有一个假依赖。
写一个32位寄存器指令没有对相应64位寄存器的假依赖,因为64位寄存器的高半部被置零。
一个XMM寄存器的局部写有对整个寄存器的假依赖,但这不适用于YMM寄存器的两个半部。256位YMM寄存器被处理为两个无关的128位XMM寄存器。
处理器将某些算术标记处理为无关。例如,仅修改进位标记的指令没有对零标记的假依赖。
19.15. 依赖打破指令
将一个寄存器置零的常见方式是XOR EAX, EAX或SUB EBX, EBX。处理器知道某些指令与寄存器的之前值无关,如果两个输入寄存器是相同的。以下指令被识别为与输入无关,如果两个输入操作数是相同的寄存器:XOR,SUB,SBB(仅依赖进位标记),CMP,PXOR,PANDN,PSUBx,PCMPEQx,PCMPGTx,XORPS,XORPD,ANDNPS,ANDNPD,但ANDN不是。
由于寄存器局部的处理,这不能作用于8位与16位寄存器,但它工作于32位与64位通用寄存器,以及各种尺寸的向量寄存器。一个寄存器与自身的浮点减法与比较从不被视为与输入无关,因为结果依赖于输入是否为NAN。
19.16. 分支与循环
分支预测机制在第26页描述。对可高效预测的16字节代码中的分支数,没有限制。
通常跳转有每2时钟周期一个被采用跳转的吞吐率。这包括直接跳转,间接跳转,调用,返回与被采用分支。不过,最多5条指令的微循环可在1时钟周期里执行一次迭代。这里,融合的比较与分支指令算作1个。小循环的对齐与指令长度不重要,因为小循环包含在μop缓存里。
不被采用的分支有每时钟周期两个的吞吐率。
19.17. 缓存与内存访问
缓存 | Ryzen |
μop | 每核2k,8路,32组,每行8 μop |
1级代码 | 每核64 kB,4路,256组,每行64 B。时延4 |
1级数据 | 每核32 kB,8路,64组,每行64 B。时延4 |
2级 | 每核512 kB,8路,1024组。时延17 |
3级 | 每4核16 MB,16路,8192组,每行64 B。时延40 |
表19.2. AMD Ryzen缓存大小
数据缓存有两个128位端口,可用于读或写。它可以在同一个时钟周期执行两次读或者一次读与一次写。到所有缓存级的数据带宽是256位(32字节)。有72个读缓冲与44个写缓冲。
当内存读的地址与前面一个写相隔0x1000倍数字数时,有一个假依赖。
在大多数情形里,自动硬件预取比显式软件预取更高效。
19.18. 写转发暂停
对一个后续读的写转发在所有情形下工作良好,包括读一个写入数据的局部。非对齐读与写没有或很少惩罚,除了在跨越页边界时。与之前的写部分重叠的读有6 ~ 7时钟周期的惩罚。
19.19. 多线程并发
处理器可以在每个核运行两个线程。这是合理的,因为每个核的吞吐率如此高,它很少被单个线程用完。
通常,在两个线程运行在同一个核里时,每个线程将得到一半的资源。μop队列平均地在两个线程间分配,因此每个线程得到最大吞吐率的一半。缓存、分支预测器、执行单元及大多数资源完全在两个线程间共享。
CPU核被组织为4个核一个的、称为CPU复合体(CPU complexe)的子单元。CPU复合体内的线程间通讯比CPU复合体间要快。
19.20. 节能与倍频
Ryzen处理器通过时钟选通不使用的执行单元,进取地节能。时钟频率依据工作负荷与芯片温度自动改变。在我的测试中,在硬盘访问是限制因素,时钟频率通常低至通常频率的8%。在一非常长串CPU密集代码后,时钟频率增加到114%。在所有核都活动时,因为增加的温度,最大时钟频率不能维持。
获得一致的性能测量是困难的,因为时钟频率一直在剧烈变化。使用一个长的虚拟计算序列,有助于预热处理器,但时钟计数仍然不太对。用于测量小代码片段执行时间的时间戳计数器(TSC),以名义频率计数。处理器有另一个类似于Intel处理器的核心时钟计数器的,称为实际执行频率时钟计数器(APERF)。但是,APERF计数器仅能在内核模式中读取,不像运行在用户模式测试程序可访问的TSC。目前的报告基于以下方式计算的时钟计数:在运行测试代码前后,在设备驱动里读取TSC与APERF计数器。以这个方式获取的TSC与APERF计数的比例,用作一个适用于所有在测试代码运行期间得到的TSC计数的修正因子。这个方法是笨拙的,但结果看起来相当精确,除了在测试期间频率变化显著的情形。在手册4“指令表”中列出的时钟计数也是用这个方法得到的。测试代码可在www.agner.org/optimize/#testp获得。
19.21. AMD Ryzen中的瓶颈
在Ryzen中每个核的吞吐率比任何之前的AMD或Intel x86处理器都高,除了256位向量指令。合适μop缓存的循环有每时钟周期5条指令或6个μop的吞吐率。不能放入μop缓存的代码有每时钟周期4条指令或6个μop或大约16字节代码的吞吐率,取决于先到达谁。对有大循环的CPU密集代码,16字节的获取速度很可能是瓶颈。
大多数指令由2、3或4个执行单元支持,因此同时执行多条类型相同的指令是可能的。指令时延通常很低。
256位向量指令被分解为两个μop。因此,包含许多256位向量指令的代码片段可能受执行单元的限制。这里最大吞吐率是每时钟周期4个向量μop,或总计6个μop,如果至少三分之一μop使用通用寄存器。
在两个线程运行在同一个核上时,每线程的吞吐率是上面的一半。但每个核的容量比单个线程应用所需要大。因此,与类似的Intel处理器相比,Ryzen在多线程并发中更有优势。尽可能将线程间通讯保持在同一个4核CPU复合体内。如果你希望性能最优,Ryzen核极高的吞吐率,给予程序员与编译器额外的负担。显然,如果第二条指令依赖第一条指令的输出,不能并行地执行这两条指令。如果希望接近每时钟周期5条指令的最大吞吐率,避免长依赖链是重要的。
缓存相当大。这是一个显著的优势,因为在大多数情形下,缓存与内存访问最有可能是瓶颈。缓存带宽是每时钟32字节,小于与之竞争的Intel处理器。
文献
Mike Clark: "AMD and the new “Zen” High Performance x86 Core". Hot Chips Symposium, California. August 23, 2016.
Ken Mitchell & Elliot Kim: "AMD Ryzen™ CPU Optimization". Game Developers Confe-rence. San Francisco, March 2017.