《深入浅出DPDK》读书笔记(四):并行计算-SIMD是Single-Instruction Multiple-Data(单指令多数据)

本文内容为读书笔记,摘自《深入浅出DPDK》。


47.提高处理器主频率对于性能的提升作用是明显而直接的。但一味地提高频率很快会触及频率墙,因为处理器的功耗正比于主频的三次方。


48.提高并行度主要有两种方法,一种是提高微架构的指令并行度,另一种是采用多核并发。


49.Amdahl定律告诉我们,假设一个任务的工作量不变,多核并行计算理论时延加速上限取决于那些不能并行处理部分的比例。


50.资源局部化、避免跨核共享、减少临界区碰撞、加快临界区完成速率(后两者涉及多核同步控制,将在下一章中介绍)等,都不同程度地降低了不可并行部分和并发干扰部分的占比。


51.下面结合图形详细介绍了单核、多核以及超线程的概念。

通过单核结构(见图3-1),我们先认识一下CPU物理核中主要的基本组件。为简化理解,将主要组件简化为:CPU寄存器集合、中断逻辑(Local APIC)、执行单元和Cache。一个完整的物理核需要拥有这样的整套资源,提供一个指令执行线程。

图3-1 单核结构

多处理器结构指的是多颗单独封装的CPU通过外部总线连接,构成的统一计算平台,如图3-2所示。每个CPU都需要独立的电路支持,有自己的Cache,而它们之间的通信通过主板上的总线。在此架构上,若一个多线程的程序运行在不同CPU的某个核上,跨CPU的线程间协作都要走总线,而共享的数据还会付出因Cache一致性产生的开销。从内存子系统的角度,多处理器结构进一步衍生出了非一致内存访问(NUMA),这一点在第2章就有介绍。在DPDK中,对于多处理器的NUMA结构,使用Socket Node来标示,跨NUMA的内存访问是性能调优时最需要避免的。

图3-2 多处理器结构

如图3-3所示,超线程(Hyper-Threading)在一个处理器中提供两个逻辑执行线程,逻辑线程共享流水线、执行单元和缓存。该技术的本质是复用单处理器中的超标量流水线的多路执行单元,降低多路执行单元中因指令依赖造成的执行单元闲置。对于每个逻辑线程,拥有完整独立的寄存器集合和本地中断逻辑,从软件的角度,与单线程物理核并没有差异。例如,8核心的处理器使用超线程技术之后,可以得到16个逻辑线程。采用超线程,在单核上可以同时进行多线程处理,使整体性能得到一定程度提升。但由于其毕竟是共享执行单元的,对IPC(每周期执行指令数)越高的应用,带来的帮助越有限。DPDK是一种I/O集中的负载,对于这类负载,IPC相对不是特别高,所以超线程技术会有一定程度的帮助。

图3-3 超线程

如果说超线程还是站在一个核内部以资源切分的方式构成多个执行线程,多核体系结构(见图3-4)则是在一个CPU封装里放入了多个对等的物理核,每个物理核可以独立构成一个执行线程,当然也可以进一步分割成多个执行线程(采用超线程技术)。多核之间的通信使用芯片内部总线来完成,共享更低一级缓存(LLC,三级缓存)和内存。随着CPU制造工艺的提升,每个CPU封装中放入的物理核数也在不断提高。

图3-4 多核体系结构

各种架构在总线占用、Cache、寄存器以及执行单元的区别大致可以归纳为表3-1。

表3-1 并行计算的底层基础架构

一个物理封装的CPU(通过physical id区分判断)可以有多个核(通过core id区分判断)。而每个核可以有多个逻辑CPU(通过processor区分判断)。一个核通过多个逻辑CPU实现这个核自己的超线程技术。

查看CPU内核信息的基本命令如表3-2所示。

表3-2 内核信息的基本命令

处理器核数:processor cores,即俗称的“CPU核数”,也就是每个物理CPU中core的个数,例如“Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz”是10核处理器,它在每个socket上有10个“处理器核”。具有相同core id的CPU是同一个core的超线程。

逻辑处理器核心数:sibling是内核认为的单个物理处理器所有的超线程个数,也就是一个物理封装中的逻辑核的个数。如果sibling等于实际物理核数的话,就说明没有启动超线程;反之,则说明启用超线程。

系统物理处理器封装ID:Socket中文翻译成“插槽”,也就是所谓的物理处理器封装个数,即俗称的“物理CPU数”,管理员可能会称之为“路”。例如一块“Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz”有两个“物理处理器封装”。具有相同physical id的CPU是同一个CPU封装的线程或核心。

系统逻辑处理器ID:逻辑处理器数的英文名是logical processor,即俗称的“逻辑CPU数”,逻辑核心处理器就是虚拟物理核心处理器的一个超线程技术,例如“Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz”支持超线程,一个物理核心能模拟为两个逻辑处理器,即一块“Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz”有20个“逻辑处理器”。


52.线程独占

DPDK通过把线程绑定到逻辑核的方法来避免跨核任务中的切换开销,但对于绑定运行的当前逻辑核,仍然可能会有线程切换的发生,若希望进一步减少其他任务对于某个特定任务的影响,在亲和的基础上更进一步,可以采取把逻辑核从内核调度系统剥离的方法。

Linux内核提供了启动参数isolcpus。对于有4个CPU的服务器,在启动的时候加入启动参数isolcpus=2,3。那么系统启动后将不使用CPU3和CPU4。注意,这里说的不使用不是绝对地不使用,系统启动后仍然可以通过taskset命令指定哪些程序在这些核心中运行。步骤如下所示。

命令:vim /boot/grub2.cfg

在Linux kernel启动参数里面加入isolcpus参数,isolcpu=2,3。

命令:cat /proc/cmdline

等待系统重新启动之后查看启动参数BOOT_IMAGE=/boot/vmlinuz-3.17.8-200.fc20. x86_64 root=UUID=3ae47813-79ea-4805-a732-21bedcbdb0b5 ro LANG=en_US.UTF-8 isolcpus=2,3。


53.DPDK的多线程

DPDK的线程基于pthread接口创建,属于抢占式线程模型,受内核调度支配。DPDK通过在多核设备上创建多个线程,每个线程绑定到单独的核上,减少线程调度的开销,以提高性能。

DPDK的线程可以作为控制线程,也可以作为数据线程。在DPDK的一些示例中,控制线程一般绑定到MASTER核上,接受用户配置,并传递配置参数给数据线程等;数据线程分布在不同核上处理数据包。

DPDK多线程:EAL pthread和lcore Affinity(F-Stack配置文件的配置参数:lcore_mask、lcore_list)

DPDK lcore学习笔记

1. EAL中的lcore

DPDK的lcore指的是EAL线程,本质是基于pthread(Linux/FreeBSD)封装实现。Lcore(EAL pthread)由remote_launch函数指定的任务创建并管理。在每个EAL pthread中,有一个TLS(Thread Local Storage)称为_lcore_id。当使用DPDK的EAL‘-c’参数指定coremask时,EAL pthread生成相应个数lcore并默认是1:1亲和到coremask对应的CPU逻辑核,_lcore_id和CPU ID是一致的。

下面简单介绍DPDK中lcore的初始化及执行任务的注册。

(1)初始化

  • 1)rte_eal_cpu_init()函数中,通过读取/sys/devices/system/cpu/cpuX/下的相关信息,确定当前系统有哪些CPU核,以及每个核属于哪个CPU Socket。
  • 2)eal_parse_args()函数,解析-c参数,确认哪些CPU核是可以使用的,以及设置第一个核为MASTER。
  • 3)为每一个SLAVE核创建线程,并调用eal_thread_set_affinity()绑定CPU。线程的执行体是eal_thread_loop()。eal_thread_loop()的主体是一个while死循环,调用不同模块注册到lcore_config[lcore_id].f的回调函数。
RTE_LCORE_FOREACH_SLAVE(i) { 
/* 
* create communication pipes between master thread 
* and children 
*/ 
if (pipe(lcore_config[i].pipe_master2slave) < 0) 
    rte_panic("Cannot create pipe\n"); 
if (pipe(lcore_config[i].pipe_slave2master) < 0) 
    rte_panic("Cannot create pipe\n"); 
lcore_config[i].state = WAIT; 
/* create a thread for each lcore */ 
ret = pthread_create(&lcore_config[i].thread_id, NULL, 
              eal_thread_loop, NULL); 
if (ret ! = 0) 
    rte_panic("Cannot create thread\n"); 
} 

(2)注册

不同的模块需要调用rte_eal_mp_remote_launch(),将自己的回调处理函数注册到lcore_config[].f中。以l2fwd为例,注册的回调处理函数是l2fwd_launch_on_lcore()。

rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, NULL, CALL_MASTER);

DPDK每个核上的线程最终会调用eal_thread_loop()--->l2fwd_launch_on_lcore(),调用到自己实现的处理函数。

最后,总结整个lcore启动过程和执行任务分发,可以归纳为如图3-5所示。

2. lcore的亲和性

默认情况下,lcore是与逻辑核一一亲和绑定的。带来性能提升的同时,也牺牲了一定的灵活性和能效。在现网中,往往有流量潮汐现象的发生,在网络流量空闲时,没有必要使用与流量繁忙时相同的核数。按需分配和灵活的扩展伸缩能力,代表了一种很有说服力的能效需求。于是,EAL pthread和逻辑核之间进而允许打破1:1的绑定关系,使得_lcore_id本身和CPU ID可以不严格一致。EAL定义了长选项“--lcores”来指定lcore的CPU亲和性。对一个特定的lcore ID或者lcore ID组,这个长选项允许为EAL pthread设置CPU集。

格式如下:

--lcores=' <lcore_set>[@cpu_set][, <lcore_set>[@cpu_set], ...]’ 

其中,‘lcore_set’和‘cpu_set’可以是一个数字、范围或者一个组。数字值是“digit([0-9]+)”;范围是“<number>-<number>”; group是“(<number—range>[, <number—ran ge>, ...])”。如果不指定‘@cpu_set’的值,那么默认就使用‘lcore_set’的值。这个选项与corelist的选项‘-l’是兼容的。

For example, "--lcores='1,2@(5-7), (3-5)@(0,2), (0,6),7-8' " which means start 9 EAL thread; 
lcore 0 runs on cpuset 0x41 (cpu 0,6); 
lcore 1 runs on cpuset 0x2 (cpu 1); 
lcore 2 runs on cpuset 0xe0 (cpu 5,6,7); 
lcore 3,4,5 runs on cpuset 0x5 (cpu 0,2); 
lcore 6 runs on cpuset 0x41 (cpu 0,6); 
lcore 7 runs on cpuset 0x80 (cpu 7); 
lcore 8 runs on cpuset 0x100 (cpu 8).

图3-5 lcore初始化及执行任务分发

这个选项以及对应的一组API(rte_thread_set/get_affinity())为lcore提供了亲和的灵活性。lcore可以亲和到一个CPU或者一个CPU集合,使得在运行时调整具体某个CPU承载lcore成为可能。

而另一个方面,多个lcore也可能亲和到同一个核。这里要注意的是,同一个核上多个可抢占式的任务调度涉及非抢占式的库时,会有一定限制。

这里以非抢占式无锁rte_ring为例:

  • 1)单生产者/单消费者模式,不受影响,可正常使用。
  • 2)多生产者/多消费者模式且pthread调度策略都是SCHED_OTHER时,可以使用,性能会有所影响。
  • 3)多生产者/多消费者模式且pthread调度策略有SCHED_FIFO或者SCHED_RR时,建议不使用,会产生死锁。

54.使用cgroup能把CPU的配额灵活地配置在不同的线程上。cgroup是control group的缩写,是Linux内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如:CPU、内存、I/O等)的机制。DPDK可以借助cgroup实现计算资源配额对于线程的灵活配置,可以有效改善I/O核的闲置利用率。


55.指令并发与数据并行

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

指令并发

现代多核处理器几乎都采用了超标量的体系结构来提高指令的并发度,并进一步地允许对无依赖关系的指令乱序执行。这种用空间换时间的方法,极大提高了IPC,使得一个时钟周期完成多条指令成为可能。

图3-6中Haswell微架构流水线是Haswell微架构的流水线参考,从中可以看到Scheduler下挂了8个Port,这表示每个core每个时钟周期最多可以派发8条微指令操作。具体到指令的类型,比如Fast LEA,它可以同时在Port 1和Port 5上派发。换句话说,该指令具有被多发的能力。可以简单地理解为,该指令先后操作两个没有依赖关系的数据时,两条指令有可能被处理器同时派发到执行单元执行,由此该指令实际执行的吞吐率就提升了一倍。

图3-6 Haswell微架构流水线

虽然处理器内部发生的指令并发过程,对于开发者是透明的。但不同的代码逻辑、数据依赖、存储布局等,会影响CPU运行时指令的派发,最终影响程序运行的IPC。由于涉及的内容非常广泛,本书限于篇幅有限不能一一展开。理解处理器的体系结构以及微架构的设计,对于调优或者高效的代码设计都会很有帮助。这里推荐读者阅读64-ia-32架构优化手册,手册中会从前端优化、执行core优化、访存优化、预取等多个方面讲解各类技巧。

“单指令多数据”给了我们这样一种可能,即使某条指令本身不再能被并(多)发,我们依旧可以从数据位宽的维度上提升并行度,从而得到整体性能提升。

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数据单元数。

图3-7 典型SIMD操作

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,可以用尽最大带宽。

表3-3 Haswell微架构中第一级Cache参数

对于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)指令集。

图3-8128位宽和256位宽SIMD寄存器

实战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; 

系列文章

《深入浅出DPDK》读书笔记(一):基础部分知识点

《深入浅出DPDK》读书笔记(二):网卡的读写数据操作

《深入浅出DPDK》读书笔记(三):NUMA - Non Uniform Memory Architecture 非统一内存架构

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页