《深入浅出DPDK》—第3章3.2节指令并发与数据并行

简介: 前面我们花了较大篇幅讲解多核并发对于整体性能提升的帮助,从本节开始,我们将从另外一个维度——指令并发,站在一个更小粒度的视角,去理解指令级并发对于性能提升的帮助。

本节书摘来自华章出版社《深入浅出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

虽然处理器内部发生的指令并发过程,对于开发者是透明的。但不同的代码逻辑、数据依赖、存储布局等,会影响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

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

对于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

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;
相关文章
|
2月前
|
缓存 编译器 程序员
C/C++编译器并行优化技术:并行优化针对多核处理器和多线程环境进行优化,以提高程序的并行度
C/C++编译器并行优化技术:并行优化针对多核处理器和多线程环境进行优化,以提高程序的并行度
90 0
|
2月前
|
存储
RTOS多线程操作的基本原理与实现
RTOS多线程操作的基本原理与实现
56 0
|
3月前
|
Rust 并行计算 安全
Rust中的并行与并发优化:释放多核性能
Rust语言以其内存安全和高效的并发模型在并行计算领域脱颖而出。本文深入探讨了Rust中的并行与并发优化技术,包括使用多线程、异步编程、以及并行算法等。通过理解并应用这些技术,Rust开发者可以有效地利用多核处理器,提高程序的性能和响应能力。
Linux设备驱动中的并发
并发就是多个执行单元或多个进程并行执行,而这多个执行单元对资源进行共享,比如访问同一个变量或同一个硬件资源,这个时候就很容易出现竞态(说简单点就是竞争同一个"女朋友")。
|
Java
28 看懂java线程,了解并行、并发一篇就够了(☆)
1 基本概念 程序(program):就是我们写的代码
123 0
|
Java 调度
多线程基本概念(并发与并行、线程与进程)和入门案例
多线程基本概念(并发与并行、线程与进程)和入门案例
|
SQL 开发框架 算法
C#多线程开发-任务并行库04
C#多线程开发-任务并行库04
131 0
C#多线程开发-任务并行库04
|
并行计算
CUDA 动态并行 【读书笔记】
CUDA 动态并行 【读书笔记】
95 0
CUDA 动态并行 【读书笔记】
|
并行计算 Java 数据挖掘
对于并行和并行概念上的理解与总结
并行和并行概念上的理解与总结
1103 0