3. DPDK:更好的压榨cpu--并行计算

1. 引言

像资本家一样思考,要想提升产量,那么如何对待工人,无非两条路:一是让他加班加点,二是让他动作更快、更聪明。加班有上限,工人累了效率反而下降;聪明地安排动作,就像工人一手搬两个箱子、一边切菜一边炒菜一样,产量蹭蹭上涨。这就是"压榨剩余价值"的方式------不是延长工时,就是压榨操作空间,最终让每个工人的价值最大化。

我们常说要提升处理器性能,主要有两条路:

  1. 让每个时钟周期干更多活 ------ 也就是提升 IPC(Instructions Per Cycle,每周期指令数);
  2. 让时钟跑得更快 ------ 提高主频。

提高主频最直观:频率越高,单位时间内能执行的指令就越多。但问题也很明显------功耗会随频率的三次方增长 。换句话说,你主频翻一倍,功耗可能涨八倍,芯片很快就变成"暖手宝"了。所以提高主频并不能无限制地堆上去,这就是我们常说的"频率墙"。

那怎么办?只能从另一条路突破:提高IPC,也就是并行度

简单理解就是:CPU 不再靠单核蛮干,而是想办法"并行干活"。要做到这一点,CPU 厂商主要有两种办法:

  • 一种是从微架构层面提升------让指令之间可以更并行地执行,比如乱序执行、流水线优化等;
  • 另一种是直接多核并发------多个核心同时跑不同的任务。

这两条思路也正是 DPDK 性能优化的核心。DPDK 本身就是为高性能网络设计的,它在用户态直接绕过内核,尽可能地发挥硬件的并行能力。

2. CPU、亲和性与 DPDK 多线程

我们已经知道 DPDK 的性能优化核心是"并行计算"。要理解它,先要从最底层的硬件结构------CPU 的核心与线程------讲起。


2.1 CPU 的层级结构:物理核、逻辑核和超线程

很多人说"我这台服务器是 40 核的",其实这个"核"到底指什么?在 Linux 系统里,一个 CPU 通常可以拆成三层来看:

层级 含义 示例
物理 CPU(Socket) 主板上插的那一颗芯片。通过 physical id 区分。 例如一台双路服务器有 2 个 Socket。
核心(Core) 每个物理 CPU 里的独立执行单元。通过 core id 区分。 比如 Xeon E5-2680 v2 是 10 核。
逻辑 CPU(Processor) 每个核心的"超线程"实例。通过 processor 区分。 如果开启超线程,一个核心会有 2 个逻辑 CPU。

简单来说:

逻辑 CPU = 物理 CPU × 每核核心数 × 每核线程数

举个例子:

Xeon E5-2680 v2 是 10 核、2 线程(超线程开启),

那么单颗 CPU 有 20 个逻辑 CPU;

双路系统就是 40 个逻辑 CPU。

查看这些信息的命令很简单(表 3-2):

bash 复制代码
lscpu                # 查看所有 CPU 拓扑信息
cat /proc/cpuinfo    # 查看每个逻辑核的 id、core id、socket id

2.2 CPU 亲和性(Core Affinity)

当系统进入多核时代,一个新问题出现了:

"线程应该跑在哪个核上比较好?"

如果线程在不同 CPU 核之间频繁迁移,会导致 Cache 不命中上下文切换频繁 ,性能大打折扣。

因此 Linux 提供了 CPU 亲和性(Affinity) 机制,让线程固定在特定的核上


2.2.1 Linux 内核的亲和性机制

在 Linux 中,每个线程都有一个结构体 task_struct,里面有个关键字段 cpus_allowed

它是一个位掩码,用来标记这个线程"可以在哪些逻辑 CPU 上运行"。

  • 所有位都是 1 → 可以跑在任意 CPU 上(默认状态)。
  • 只设置特定位 → 固定到某些 CPU 上运行。

Linux 提供了两个系统调用来操作:

scss 复制代码
sched_set_affinity(pid, size, mask);  // 绑定线程到指定 CPU
sched_get_affinity(pid, size, mask);  // 查询当前绑定信息

子线程会继承父线程的 CPU 亲和性,因此需要时可以显式重新绑定。


2.2.2 为什么要使用亲和性?

绑定线程的好处非常实际:

  • Cache 命中率更高:线程始终在同一个核上跑,L1/L2 缓存命中率提升明显;
  • 减少上下文切换:不再频繁迁移,CPU 调度负担更小;
  • NUMA 性能更稳定:减少跨节点访问,避免内存访问延迟。

特别是在 DPDK 这种高性能网络场景 下,绑定核几乎是"标配操作"。

每个核专门负责接收或发送数据包,不会被系统调度打扰。


2.2.3 进一步隔离:线程独占

如果希望线程彻底独占 CPU 核,可以使用内核启动参数:

ini 复制代码
isolcpus=2,3

这样系统启动后,CPU 2 和 3 不会被内核调度器使用, 可以留给 DPDK 这样的高性能任务。

验证方法:

bash 复制代码
cat /proc/cmdline

你会看到类似输出:

ini 复制代码
BOOT_IMAGE=/boot/vmlinuz-3.17.8... isolcpus=2,3

此时可用 taskset 手动把进程绑定到这些核上。


2.3 DPDK 的多线程模型

DPDK 在多核架构上充分利用了 Linux 的 pthread 机制,它创建多个线程(称为 lcore),每个绑定到一个逻辑核上,实现高效的并行处理。


2.3.1 EAL 与 lcore 的关系

DPDK 的每个线程本质上是一个 pthread,在 DPDK 中叫 lcore。 这些线程由 EAL(Environment Abstraction Layer)管理:

  • 初始化阶段

    1. rte_eal_cpu_init():读取 /sys/devices/system/cpu 确定 CPU 核信息;
    2. eal_parse_args():解析 -c 参数,确定可用核;
    3. 为每个 Slave 核创建线程并调用 eal_thread_set_affinity() 绑定 CPU。
  • 任务分发阶段

    模块通过 rte_eal_mp_remote_launch() 注册回调函数,比如:

    objectivec 复制代码
    rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, NULL, CALL_MASTER);

    每个核都会执行自己的任务函数,实现真正的多核并行。


2.3.2 lcore 的灵活绑定

默认情况下,lcore 与逻辑核是一对一绑定的。

但在生产环境中,流量存在"潮汐效应"------有时高、有时低。

为了提高能效,DPDK 支持更灵活的绑定方式:

erlang 复制代码
--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'

意思是:

  • lcore 0 绑定到 CPU 0,6
  • lcore 1 → CPU 1
  • lcore 2 → CPU 5,6,7
  • lcore 3,4,5 → CPU 0,2
  • ...依此类推。

这种映射方式让你可以动态调整线程与核的关系,更适合多租户或能耗敏感场景。


2.3.3 自定义 pthread 的支持

除了 DPDK 自建的 lcore 线程,用户也可以自己创建普通 pthread 来运行 DPDK 的代码。

这种线程的 lcore_id 默认为 LCORE_ID_ANY,可以使用大多数 DPDK 库。

不过部分模块(例如 Mempool 的每核缓存)不会生效,性能可能略有下降。


2.3.4 管理计算资源:配合 cgroup 使用

当网络流量较小时,不必让所有核都处于工作状态。 这时可以借助 cgroup(control group) 控制 CPU 配额,为不同线程分配计算资源。

这在云化网关、虚拟化等场景中尤其常见,可以做到:

  • 大流量时:多核并行处理;
  • 低流量时:动态回收空闲核,节能省资源。

3. 指令并发与数据并行

上一节我们聊了"多核并发",核心思想是"多个工人一起干活 "。 但其实在单个核心内部,也有提升性能的空间------这就是指令并发(Instruction-Level Parallelism, ILP)数据并行(Data Parallelism)

你可以这么理解:

并行类型 类比 核心思路
多核并发 多个工人同时工作 每个核独立处理一部分任务
指令并发 一个工人双手并用 一核内同时执行多条无依赖指令
数据并行 一个工人一次搬四个箱子 一条指令操作多个数据

3.1 指令并发:CPU 的"多手操作术"

现代CPU几乎都采用超标量(Superscalar)架构。

这意味着:

CPU不再是"一次只执行一条指令",而是能并行执行多条指令,只要它们之间互不依赖。


3.1.1 CPU 就像一个泡茶工

假设你要泡茶,步骤是:

  1. 烧水
  2. 拿杯子
  3. 加茶叶
  4. 倒水

如果按顺序执行,要等水烧开才能拿杯子。

但"超标量CPU"的思维是:

"我烧水的同时手又不是闲的,可以先去拿杯子!"

这样,烧水和拿杯子就并行完成了。 这就是指令级并行(Instruction-Level Parallelism, ILP)的核心思想。


3.1.2 CPU 是怎么做到的?

以 Intel Haswell 架构为例,它的每个核心里都有一个 "调度器"(Scheduler), 调度器下面挂了 8 个执行端口(Ports)

端口编号 功能示例
Port 0/1 整数/浮点计算
Port 2/3 加载(Load)
Port 4 存储(Store)
Port 5 分支预测/地址计算
Port 6/7 特殊指令/备用

也就是说,一个核心每个时钟周期可以发出最多 8 条微指令(uops) 。 像 Fast LEA 这样的指令,甚至能同时派发到 Port 1 和 Port 5 ------ 等于一条指令可以"一鱼两吃",同时算两个独立数据。


3.1.3 IPC 的提升原理

IPC(Instructions Per Cycle):每周期能执行的指令数。

指令能并行执行得越多,IPC 就越高,性能自然越强。

但要注意:

  • 如果指令之间有依赖(A 依赖 B 的结果),那就不能并行。
  • 如果访存太频繁或数据不对齐,调度器也可能卡住。

这也是为什么同样的算法,不同写法性能差别很大。 理解微架构(如流水线、乱序执行、Cache结构)能帮助你写出更"CPU友好"的代码。


3.2 单指令多数据:让一条指令干四份活

指令并发是"多条指令并行",而数据并行(SIMD)则是反过来------

用"一条指令"同时处理"多个数据"。


3.2.1 举个例子:四组加法

假设要计算两组数组相加:

X组 Y组 结果
X1 Y1 X1 + Y1
X2 Y2 X2 + Y2
X3 Y3 X3 + Y3
X4 Y4 X4 + Y4

普通CPU要执行 4 次加法; SIMD 指令只要 一条指令 就能全部完成。

这就是 SIMD(Single Instruction, Multiple Data) ------ 单指令多数据。


3.2.2 SIMD的底层原理

SIMD依赖的是"宽寄存器(Wide Register) "。

指令集 寄存器 位宽 能同时处理的数据数(以64bit为例)
SSE XMM 128 bit 2 个
AVX YMM 256 bit 4 个
AVX-512 ZMM 512 bit 8 个

这就像:

普通寄存器一次能搬 1 箱货,SIMD 寄存器一次能搬 4 箱。


3.2.3 性能的关键:Cache 带宽利用率

以 Haswell 架构为例:

  • 每周期可执行两个 32B 的 Load(共64B)+ 一个 32B 的 Store。
  • L1 Cache 每周期峰值带宽 ≈ 96B。

而一条 AVX 指令的宽度是 256bit(32B) , 这意味着它刚好能吃满 L1 的访存带宽,不浪费任何"路宽"。

操作 带宽占用 说明
普通64bit Load 8B 只能用到 1/12 带宽
SSE Load 16B 利用 1/6 带宽
AVX Load 32B 完美吃满单通道带宽

因此,在像 DPDK 这种 I/O 密集 的框架里, 使用 SIMD 可以显著减少 CPU "空等数据" 的时间。


3.2.4 但要注意:SIMD 不是万能药

SIMD 适合数据规律、结构简单的场景。 如果数据格式复杂或字段很窄,SIMD 的对齐开销反而可能抵消收益。 所以 DPDK 里一般只在关键路径、高带宽操作 上启用 SIMD,比如 memcpy


3.3 实战:DPDK 的 rte_memcpy

DPDK 的 rte_memcpy() 就是利用 SIMD 提速的经典例子。

拷贝数据看似简单,但 DPDK 的思路是:

"既然拷贝不可避免,那就让CPU一次拷多一点。"

在 Intel Haswell 平台上:

  • 每周期能执行两条 Load + 一条 Store;
  • 支持 SSE/AVX;
  • Cache 带宽能完全支持 256bit 操作。

因此,DPDK 会:

  1. 使用最大支持宽度的 Load/Store(256bit);
  2. 优先保证 Store 地址对齐
  3. 让两个 Load 指令并行执行,弥补非对齐 Load 的损失。
ini 复制代码
/**
 * 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 += dstofss;
dst += dstofss;

对于老架构(如 Sandy Bridge),非对齐代价更高,DPDK 会让 Load 也对齐,甚至多执行一条 Load 来换性能。


DPDK 的哲学: 不浪费一个CPU周期,不留一分带宽闲着。

相关推荐
两万五千个小时4 小时前
LangChain 入门教程:06LangGraph工作流编排
人工智能·后端
oak隔壁找我4 小时前
MyBatis的MapperFactoryBean详解
后端
王道长AWS_服务器4 小时前
AWS Elastic Load Balancing(ELB)—— 多站点负载均衡的正确打开方式
后端·程序员·aws
oak隔壁找我4 小时前
Spring BeanFactory 和 FactoryBean 详解
后端
用户4099322502124 小时前
只给表子集建索引?用函数结果建索引?PostgreSQL这俩操作凭啥能省空间又加速?
后端·ai编程·trae
oak隔壁找我4 小时前
SpringMVC 教程
后端
用户34325962788164 小时前
Spring AI Alibaba中使用Redis Vector报错修改过程
后端
oak隔壁找我4 小时前
MyBatis和SpringBoot集成的原理详解
后端
oak隔壁找我4 小时前
SpringBoot @Import 注解详解
后端