深入浅出DPDK学习笔记(4)——— 并行计算
处理器性能提升主要有两个途径,一个是提高IPC(每个时钟周期内可以执行的指令条数),另一个是提高处理器主频率。每一代微架构的调整可以伴随着对IPC的提高,从而提高处理器性能,只是幅度有限。而提高处理器主频率对于性能的提升作用是明显而直接的。但一味地提高频率很快会触及频率墙,因为处理器的功耗正比于主频的三次方。
所以,最终要取得性能提升的进一步突破,还是要回到提高IPC这个因素。经过处理器厂商的不懈努力,我们发现可以通过提高指令执行的并行度来提高IPC。而提高并行度主要有两种方法,一种是提高微架构的指令并行度,另一种是采用多核并发。这一章主要就分享这两种方法在DPDK中的实践,并在指令并行方法中上进一步引入数据并发的介绍。
多核性能和可扩展性
追求性能水平扩展
多核处理器是指在一个处理器中集成两个或者多个完整的内核(及计算引擎)。
Amdahl定律告诉我们,假设一个任务的工作量不变,多核并行计算理论时延加速上限取决于那些不能并行处理部分的比例。换句话说,多核并行计算下时延不能随着核数增加而趋于无限小。该定律明确告诉我们,利用多核处理器提升固定工作量性能的关键在于降低那些不得不串行部分占整个任务执行的比例。更多信息可以参考[Ref3-1]。
对于DPDK的主要应用领域——数据包处理,多数场景并不是完成一个固定工作量的任务,更主要关注单位时间内的吞吐量。Gustafson定律对于在固定工作时间下的推导给予我们更多的指导意义。它指出,多核并行计算的吞吐率随核数增加而线性扩展,可并行处理部分占整个任务比重越高,则增长的斜率越大。带着这个观点来读DPDK,很多实现的初衷就豁然开朗。资源局部化、避免跨核共享、减少临界区碰撞、加快临界区完成速率(后两者涉及多核同步控制,将在下一章中介绍)等,都不同程度地降低了不可并行部分和并发干扰部分的占比。
多核处理器
通过单核结构(见图3-1),我们先认识一下CPU物理核中主要的基本组件。为简化理解,将主要组件简化为:CPU寄存器集合、中断逻辑(Local APIC)、执行单元和Cache。一个完整的物理核需要拥有这样的整套资源,提供一个指令执行线程。

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

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

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

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

亲和性
简单地说,CPU亲和性(Core affinity)就是一个特定的任务要在某个给定的CPU上尽量长时间地运行而不被迁移到其他处理器上的倾向性。这意味着线程可以不在处理器之间频繁迁移。这种状态正是我们所希望的,因为线程迁移的频率小就意味着产生的负载小。
Linux内核包含了一种机制,它让开发人员可以编程实现CPU亲和性。这意味着应用程序可以显式地指定线程在哪个(或哪些)处理器上运行。
Linux内核对亲和性的支持
在Linux内核中,所有的线程都有一个相关的数据结构,称为task_struct。这个结构非常重要,原因有很多;其中与亲和性相关度最高的是cpus_allowed位掩码。这个位掩码由n位组成,与系统中的n个逻辑处理器一一对应。具有4个物理CPU的系统可以有4位。如果这些CPU都启用了超线程,那么这个系统就有一个8位的位掩码。
如果针对某个线程设置了指定的位,那么这个线程就可以在相关的CPU上运行。因此,如果一个线程可以在任何CPU上运行,并且能够根据需要在处理器之间进行迁移,那么位掩码就全是1。实际上,在Linux中,这就是线程的默认状态。
Linux内核API提供了一些方法,让用户可以修改位掩码或查看当前的位掩码:
- sched_set_affinity()(用来修改位掩码)
- sched_get_affinity()(用来查看当前的位掩码)
注意,cpu_affinity会被传递给子线程,因此应该适当地调用sched_set_affinity。
为什么应该使用亲和性
将线程与CPU绑定

本文探讨了DPDK在多核处理器环境下的性能优化策略,包括使用CPU亲和性和线程独占技术来减少线程迁移,提高Cache命中率。介绍了DPDK中的lcore概念及其初始化和任务注册过程,并讨论了指令并发与数据并行的重要性。
最低0.47元/天 解锁文章
1312

被折叠的 条评论
为什么被折叠?



