本节书摘来自华章出版社《深入浅出DPDK》一书中的第3章,第3.2节指令并发与数据并行,作者朱河清,梁存铭,胡雪焜,曹水 等,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
3.2 指令并发与数据并行
前面我们花了较大篇幅讲解多核并发对于整体性能提升的帮助,从本节开始,我们将从另外一个维度——指令并发,站在一个更小粒度的视角,去理解指令级并发对于性能提升的帮助。
3.2.1 指令并发
现代多核处理器几乎都采用了超标量的体系结构来提高指令的并发度,并进一步地允许对无依赖关系的指令乱序执行。这种用空间换时间的方法,极大提高了IPC,使得一个时钟周期完成多条指令成为可能。
图3-6中Haswell微架构流水线是Haswell微架构的流水线参考,从中可以看到Scheduler下挂了8个Port,这表示每个core每个时钟周期最多可以派发8条微指令操作。具体到指令的类型,比如Fast LEA,它可以同时在Port 1和Port 5上派发。换句话说,该指令具有被多发的能力。可以简单地理解为,该指令先后操作两个没有依赖关系的数据时,两条指令有可能被处理器同时派发到执行单元执行,由此该指令实际执行的吞吐率就提升了一倍。
![5c7ee5964e6d8c53ebe51a71d623c21629b2dd2f](https://i-blog.csdnimg.cn/blog_migrate/e3ffede030bfaf73c1822d20128635e1.png)
虽然处理器内部发生的指令并发过程,对于开发者是透明的。但不同的代码逻辑、数据依赖、存储布局等,会影响CPU运行时指令的派发,最终影响程序运行的IPC。由于涉及的内容非常广泛,本书限于篇幅有限不能一一展开。理解处理器的体系结构以及微架构的设计,对于调优或者高效的代码设计都会很有帮助。这里推荐读者阅读64-ia-32架构优化手册,手册中会从前端优化、执行core优化、访存优化、预取等多个方面讲解各类技巧。
3.2.2 单指令多数据
在进入到什么是“单指令多数据”之前,先简单认识一下它的意义。“单指令多数据”给了我们这样一种可能,即使某条指令本身不再能被并(多)发,我们依旧可以从数据位宽的维度上提升并行度,从而得到整体性能提升。
3.2.2.1 SIMD简介
SIMD是Single-Instruction Multiple-Data(单指令多数据)的缩写,从字面的意思就能理解大致的含义。多数据指以特定宽度为一个数据单元,多单元数据独立操作。而单指令指对于这样的多单元数据集,一个指令操作作用到每个数据单元。可以把SIMD理解为向量化的操作方式。典型SIMD操作如图3-7所示,两组各4个数据单元(X1,X2,X3,X4和Y1,Y2,Y3,Y4)并行操作,相同操作作用在相应的数据单元对上(X1和Y1,X2和Y2,X3和Y3,X4和Y4),4对计算结果组成最后的4数据单元数。
![92257017a5db9865b86e011cffa05aecc4164b35](https://i-blog.csdnimg.cn/blog_migrate/feb7dcc7135fcc89b0d02d453a08fbfd.png)
SIMD指令操作的寄存器相对于通用寄存器(general-purpose register,RPRS)更宽,128bit的XMM寄存器或者256bit的YMM寄存器,有2倍甚至4倍于通用寄存器的宽度(在64bit架构上)。所以,用SIMD指令的一个直接好处是最大化地利用一级缓存访存的带宽,以表3-3所示Haswell微架构中第一级Cache参数为例,每时钟周期峰值带宽为64B(load)(注:每周期支持两个load微指令,每个微指令获取最多32B数据)+32B(store)。可见,该微架构单时钟周期可以访存的最大数据宽度为32B即256bit,只有YMM寄存器宽度的单指令load或者store,可以用尽最大带宽。
![f56fe1d0ca843a8a254275af3f19f9c9bb87b375](https://i-blog.csdnimg.cn/blog_migrate/00cdc03d8ba22a28693907a1251b709b.png)
对于I/O密集的负载,如DPDK,最大化地利用访存带宽,减少处理器流水线后端因I/O访问造成的CPU失速,会对性能提升有显著的效果。所以,DPDK在多个基础库中都有利用SIMD做向量化的优化操作。然而,也并不是所有场景都适合使用SIMD,由于数据位较宽,对繁复的窄位宽数据操作副作用比较明显,有时数据格式调整的开销可能更大,所以选择使用SIMD时要仔细评估好负载的特征。
图3-8所示的128位宽的XMM和256位宽的YMM寄存器分别对应Intel? SSE(Streaming SIMD Extensions)和Intel? AVX(Advanced Vector Extensions)指令集。
![2dad64960e7a34fd6a1ea834ef5ce92008b170c3](https://i-blog.csdnimg.cn/blog_migrate/9fe1d558dba267efba2bcd9aa385af20.png)
3.2.2.2 实战DPDK
DPDK中的memcpy就利用到了SSE/AVX的特点。比较典型的就是rte_memcpy内存拷贝函数。内存拷贝是一个非常简单的操作,算法上并无难度,关键在于很好地利用处理器的各种并行特性。当前Intel的处理器(例如Haswell、Sandy Bridge等)一个指令周期内可以执行两条Load指令和一条Store指令,并且支持SIMD指令(SSE/AVX)来在一条指令中处理多个数据,其Cache的带宽也对SIMD指令进行了很好的支持。因此,在rte_memcpy中,我们使用了平台所支持的最大宽度的Load和Store指令(Sandy Bridge为128bit,Haswell为256bit)。此外,由于非对齐的存取操作往往需要花费更多的时钟周期,rte_memcpy优先保证Store指令存储的地址对齐,利用处理器每个时钟周期可以执行两条Load这个超标量特性来弥补一部分非对齐Load所带来的性能损失。更多信息可以参考[Ref3-3]。
例如,在Haswell上,对于大于512字节的拷贝,需要按照Store地址进行对齐。
/**
* Make store aligned when copy size exceeds 512 bytes
*/
dstofss = 32 - ((uintptr_t)dst & 0x1F);
n -= dstofss;
rte_mov32((uint8_t *)dst, (const uint8_t *)src);
src = (const uint8_t *)src + dstofss;
dst = (uint8_t *)dst + dstofss;
在Sandy Bridge上,由于非对齐的Load/Store所带来的的额外性能开销非常大,因此,除了使得Store对齐之外,Load也需要进行对齐。在操作中,对于非对齐的Load,将其首尾未对齐部分多余的位也加载进来,因此,会产生比Store指令多一条的Load。
xmm0 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 0 * 16));
len -= 128;
xmm1 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 1 * 16));
xmm2 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 2 * 16));
xmm3 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 3 * 16));
xmm4 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 4 * 16));
xmm5 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 5 * 16));
xmm6 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 6 * 16));
xmm7 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 7 * 16));
xmm8 = _mm_loadu_si128((const __m128i *)((const uint8_t *)src - offset + 8 * 16));
src = (const uint8_t *)src + 128;
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 0 * 16), _mm_alignr_epi8(xmm1, xmm0, offset));
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 1 * 16), _mm_alignr_epi8(xmm2, xmm1, offset));
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 2 * 16), _mm_alignr_epi8(xmm3, xmm2, offset));
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 3 * 16), _mm_alignr_epi8(xmm4, xmm3, offset));
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 4 * 16), _mm_alignr_epi8(xmm5, xmm4, offset));
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 5 * 16), _mm_alignr_epi8(xmm6, xmm5, offset));
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 6 * 16), _mm_alignr_epi8(xmm7, xmm6, offset));
_mm_storeu_si128((__m128i *)((uint8_t *)dst + 7 * 16), _mm_alignr_epi8(xmm8, xmm7, offset));
dst = (uint8_t *)dst + 128;