第一部分:Memory Management (内存管理)

news/2025/3/31 23:01:42/文章来源:https://www.cnblogs.com/Tohomson/p/18799151

DPDK 官方文档阅读 - Lcore Variables

1、Lcore variable 是什么?

逻辑核变量是DPDK框架为每个核心分配的变量,这个变量代表着框架为一个逻辑核保存的可以自定义的变量,可以使用这个变量来访问这个变量所代表的核心的内容,访问这个变量需要逻辑核变量具柄,他是一个指向变量类型的指针,但是是黑盒子指针,只能通过特定的宏访问,不能解引用。

单看官方给的定义不是很了解它到底是个什么东西,其实后面的部分有讲,他是一个可以自定义的变量,可以定义为基础类型,也可以定义为结构体,一般用于做核心上的一些简答但是重要的功能,比如收包发统计、核心状态等;

核心变量是以链表连接多个整块内存的方式来保存的。

这个变量在框架初始化的时候回给每个核心分配一个本地内存,这块内存用来保存每个核心的所有核心变量,核心变量是可以定义多个的,如果内存不够了会分配心新的内存,以头插法的方式插入链表。

2、分配Lcore变量

通过 RTE_LCORE_VAR_HANDLE 定义一个句柄。

使用 RTE_LCORE_VAR_ALLOCRTE_LCORE_VAR_INIT 分配内存并初始化句柄。

Lcore变量分配通常在模块初始化的时候做,但是实际上在任何时候都可以分配,而且它的生命周期不取决于分配的线程,而是整个框架的周期,lcore也不能被释放。

3、在DPDK中,线程可以与逻辑核变量绑定,但是任意线程都可以访问任意的逻辑和变量,应该避免非逻辑核变量持有者线程频繁访问逻辑核变量,这会带来竞争,有可能需要上锁,会带来性能开销。

RTE_LCORE_VAR_LCORE:用于访问某个lcore id对应的值;

RTE_LCORE_VAR:用于访问当前线程自己的Lcore变量;

RTE_LCORE_VAR_FOREACH:用于便利所有LCORE的Lcore变量值;

4、Lcore变量的存储

Lcore变量可以是基本类型,但是更推荐使用struct来组织更多字段;

每个lcore变量都会额外消耗sizeof(void *) 字节的内存,如果你把一个模块所有 per-core 的变量打包成一个结构体(再作为一个 lcore 变量),能更节省空间。

应用程序可以定义句柄但不立即分配变量。

每个变量值的大小不得超过 RTE_MAX_LCORE_VAR。这个大小是指“一个值”的大小,不是所有副本加起来的总大小。

一般不建议给 lcore 变量加 __rte_cache_alignedRTE_CACHE_GUARD,因为 DPDK 的布局设计已经很好地避免了 false sharing。加这些字段反而会增加缓存压力,降低性能。

lcore 变量默认初始化为零。

Lcore变量是用来做什么的:

比如在一个模块中,需要统计这个核心的收包数量、发包数量、标记这个核心是否活跃,就可以使用lcore变量来保存:

//可以使用三个变量的方式
int rx_count;
int tx_count;
bool is_active;
//可以使用结构体的方式
struct my_core_data {int rx_count;int tx_count;bool is_active;
};
RTE_LCORE_VAR_HANDLE(struct my_core_data, handle);
//初始化
memset(RTE_LCORE_VAR(handle), 0, sizeof(struct my_core_data));

lcore变量声明后就会为每个核心分配一个自己本地的Lcore变量,用于自己访问,不管启用了多啊少核心,DPDK编译的时候都会分配RTE_MAX_LCORE这个大数组的lcore变量。

关于lcore变量的访问:

步骤 说明
定义句柄 RTE_LCORE_VAR_HANDLE(type, name) 就是 type *name 的语法糖
分配变量 RTE_LCORE_VAR_ALLOC(name) 为所有 lcore 分配副本
访问当前线程值 RTE_LCORE_VAR(name) 等价于 &name[lcore_id()]
访问指定 lcore RTE_LCORE_VAR_LCORE(name, id) 访问其他 lcore 的值
遍历所有 lcore 值 RTE_LCORE_VAR_FOREACH(name, id) 用来做统计或调试
#include <rte_lcore.h>
#include <rte_lcore_var.h>
RTE_LCORE_VAR_HANDLE(uint32_t, packet_counter);	//声明handle
void init_var(void) {RTE_LCORE_VAR_ALLOC(packet_counter);
}
void process_packet(void) {// 当前线程的 lcore 变量值加一(*RTE_LCORE_VAR(packet_counter))++;
}
void print_all(void) {uint16_t lcore;RTE_LCORE_VAR_FOREACH(packet_counter, lcore) {printf("lcore %u handled %u packets\n",lcore,*RTE_LCORE_VAR_LCORE(packet_counter, lcore));}
}

Lcore 变量的存储

struct lcore_var_buffer {char data[RTE_MAX_LCORE_VAR * RTE_MAX_LCORE];struct lcore_var_buffer *prev;
};

假设RTE_MAX_LCORE = 4  → 系统最多支持 4 个逻辑核

RTE_MAX_LCORE_VAR = 128 字节 → 每个 lcore id 拥有的变量“空间块”大小

那么:char data[128 * 4] = char data[512] 字节,内存分分布如下:

+----------------------+  ← data[0] 开始
|  lcore 0 变量空间      |  ← 128 字节
|----------------------|
|  lcore 1 变量空间      |  ← 128 字节
|----------------------|
|  lcore 2 变量空间      |  ← 128 字节
|----------------------|
|  lcore 3 变量空间      |  ← 128 字节
+----------------------+  ← 共 512 字节结束

如果后续新增了lcore变量,在128字节空间内放不下了,那么会新分配内存,将增加的部分,以前插入的方式,插入到链表,使用头插法的原因是方便从最开始的地方进行清理。

所以最终我们可以总结为:

  1. 整个 data 是一个大数组(其实就是一个二维:lcore × 每核空间)
  2. 每个 lcore 的数据是连续的,访问快
  3. 每个 lcore id 的“空间切片”是一样大的(128 字节)
  4. 多个变量在每个切片中顺序排列,分配 offset 自动推进
  5. 不够用了就分配新的 buffer 接着用(形成链表)
  6. 最终在 rte_eal_cleanup() 时一起释放

变量具柄(handle)

lcore var handle 的实际值:它指向的是当前 lcore_var_buffer 中的数据区域,从 offset 处开始,表示该变量所有 lcore 实例的“起始位置”

buffer->data + offset

类型安全怎么保证?

虽然你可以自己调用 rte_lcore_var_lcore() 来拿地址,但 DPDK 更推荐使用这些宏

  • RTE_LCORE_VAR(handle):当前线程自己的值
  • RTE_LCORE_VAR_LCORE(handle, lcore_id):指定 lcore 的值

这些宏的好处是:

  • 自动返回强类型指针(指向你原始声明的类型)
  • 比直接用 void 指针更安全
  • 编译器能做类型检查,避免错误访问

lcore 变量句柄其实就是一个“起始地址”,而每个逻辑核的数据块就是这片内存的一段偏移。

想访问某个核的变量副本?直接从句柄加上 (核编号 × 步长) 就到了。

但为了避免你自己算偏移出错,DPDK 提供了宏来自动帮你完成这步,并且保证返回的指针类型正确。

使用lcore 变量带来的性能提升

lcore 变量的一个设计目标就是 提升性能

这种提升的方式主要体现在以下几点:

  • 将频繁访问的数据尽可能紧密地排列在更少的 cache line 中
  • 减少 CPU 缓存中的碎片化
  • 提高实际有效的缓存利用率和命中率(cache hit rate)

应用层是否能受益,取决于以下几个因素:

  1. 你把多少数据存到了 lcore 变量里?
  2. 你访问这些数据有多频繁?
  3. 你的程序本身对 CPU 缓存的压力有多大?
    • 比如你访问了很多其他随机内存,会冲刷掉 lcore 数据所在的 cache 行,那收益就不明显

DPDK 提供了一个性能测试工具:

lcore_var_perf_autotest

这个测试用于对比 lcore 变量和传统 lcore-id 索引数组在性能上的优劣。

但要注意:这只是一个微基准测试(micro benchmark),只反映在某种极端条件下的差异,不能完全代表实际应用的表现。

另一个重要的好处:规避硬件预取导致的 false sharing 问题

有时,即使两个核访问的数据本来不在同一个 cache line 上,也可能由于CPU 硬件预取(prefetch)行为,而间接导致 false sharing 问题。

这种预取行为:

  • CPU 自动干的,不在我们程序控制范围内
  • 不同厂商(Intel、AMD)、不同 CPU 代数、不同 BIOS 配置下行为都不同
  • 通常不会出问题,但在某些场景下,会带来严重性能抖动

使用 lcore variables 后,每个核的数据完全隔离在独立的区域中,可以有效避开这种预取带来的“误伤”。

总结:

内容
性能优化手段 把每核私有数据集中,减少 cache line 数量,提高缓存命中率
实际收益 一般是小幅提升(具体看程序访问模式和缓存压力)
传统方式的问题 即使不共享数据,CPU 的硬件预取机制也可能导致“伪共享”
lcore var 的优势 内存隔离彻底,天然防止 false sharing,规避不确定性风险

替代方案(Alternatives)

  1. Lcore Id 索引静态数组(Lcore Id Indexed Static Arrays)

这会导致什么问题?

为了避免不同 lcore 的数据在同一 cache line 中导致 false sharing,你就必须:

  1. __rte_cache_aligned 把每个元素对齐到 cache line
  2. 使用 RTE_CACHE_GUARD 加上“守卫空间”做隔离
  3. 确保内存分配时本身也按 cache line 对齐

即使你都做到了,也不能完全避免因为以下原因带来的 false sharing 风险:

  • CPU 硬件的 预取机制(prefetching)
  • 推测执行(speculative execution) 带来的意外内存访问

有时这些机制会访问到下一条 cache line,即使两个线程本来没访问同一块数据,也可能发生冲突

lcore variables 的优势:

lcore variables 的内存布局方式(每个 lcore 一整块独立空间)正好契合了 CPU 的预期行为

  • 每个 lcore 的所有数据集中在一起
  • 不再需要复杂的 padding、对齐处理
  • 避免 CPU 硬件预取引发的 false sharing
  • 整体 内存局部性更好
项目 静态数组方案 lcore variable 方案
内存布局 所有核的数据都放一起(模块为单位) 每个核的数据独立(lcore 为单位)
避免 false sharing 的手段 需要额外对齐、padding 天然避免,无需对齐
对 CPU 缓存友好程度
是否会被 CPU 预取机制误伤 可能 不太可能
  1. 线程局部存储(Thread Local Storage)

另一个替代 rte_lcore_var.h 的方案是使用 rte_per_lcore.h 提供的 TLS(线程局部存储)API,底层基于:

  • __thread(GCC 扩展)
  • _Thread_local(C11 标准)

TLS 和 lcore variable 有哪些区别?

特性 TLS(__thread/_Thread_local) lcore variable(rte_lcore_var)
生命周期 绑定线程:线程退出后变量消失 独立于线程,谁创建的都可以访问
初始化时机 线程启动后才初始化(lazy) 分配后立即可用
是否作用于所有线程 是,所有线程(包括非 EAL)都有副本 只有带 lcore id 的线程才有副本
线程频繁创建销毁 会频繁触发 TLS 初始化,可能开销大 无影响,变量独立于线程生命周期
是否能跨线程共享指针 有风险,C11 标准下不推荐 安全,所有线程都能访问
内存布局 类似,但每线程分散 按 lcore 聚合,更集中

总结:

场景 推荐方案
线程模型复杂、动态 使用 TLS
按核调度、线程稳定 使用 lcore variable
性能关键、需避免 false sharing 使用 lcore variable
小型模块、临时存储 TLS 更简单

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/907463.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

flutter:用http库下载文件

一,安装第三方库 地址: https://pub.dev/packages/http 编辑pubspec.yaml: dependencies:flutter:sdk: flutterpath_provider: ^2.1.5http: ^1.3.0 然后点击 pub get 二,代码: import package:flutter/material.dart; import package:http/http.dart show get; import packa…

flutter:从接口获取json数据后并解析

一,代码: dart代码:model class GoodsListItem {String name;String desc;int id;GoodsListItem(this.name,this.desc, this.id) {}GoodsListItem.fromJson(Map<String, dynamic>json):name=json["name"],desc = json["desc"],id = json["id&q…

图解 CSS 选择器

https://zhuanlan.zhihu.com/p/715717977CSS 选择器用于选择 HTML 元素并将样式应用于它们。使用这些选择器,可以定义特定条件下应用哪些样式。除了普通的选择器外,还有伪类和伪元素,用于选择具有特定状态或特定部分的元素,并将样式应用于它们。本文将通过图文并茂的方式展…

二分图学习笔记

使用题单:二分图 - 从入门到入土。 二分图概念 对于一个图,如果能够把它的点集恰好分成两个部分,使得这第一个部分里面的点两两不连边,第二个部分里面的点也两两不连边,则该图是二分图。或者说每一条边都横跨了两个集合。 举个例子:这个图是二分图,因为我们可以将它分成…

SciTech-EECS-Signal-OpAmp(Operational Amplifier,运算放大器): Gain增益放大倍数计算公式 + 分流器采样百安级大电流的微电压信号 + 微电压信号放大

SciTech-EECS-Signal-OpAmp(Operational Amplifier,运算放大器): Gain增益放大倍数计算公式## 分流器采样百安级大电流的微电压信号 OpAmp(运算放大器)微电压信号放大 如上图所示,\(\large V_{out} = V_{in} \times (1+ \dfrac{R_{2}}{R_{1}})\) TL431+MOS管,充满自停的充电器…

读DAMA数据管理知识体系指南34数据仓库和商务智能概念

读DAMA数据管理知识体系指南34数据仓库和商务智能概念1. 业务驱动因素 1.1. 主要驱动力是运营支持职能、合规需求和商务智能活动 1.2. 用数据来证明他们是合规的,因为数据仓库中包含历史数据,所以经常被用来响应这类要求 1.3. 商务智能支持一直是建设数据仓库的主要原因 2. 目…

环境检测 温湿度 噪声 建大仁科

环境检测 温湿度 噪声 建大仁科 1、温湿度 wifi版本 配置软件2、噪声 wifi 版本 配置软件 android手机上安装 蓝牙连接配置3、平台下载 RS-RJ-K监控平台-平台软件-温湿度传感器产品说明书下载及选型erwa.cn 二娃测试备忘

C# 13 中的新增功能实操

前言 今天大姚带领大家一起来看看 C# 13 中的新增几大功能,并了解其功能特性和实际应用场景。 前提准备 要体验 C# 13 新增的功能可以使用最新的 Visual Studio 2022 版本或 .NET 9 SDK 尝试这些功能。 Visual Studio 2022安装https://visualstudio.microsoft.com/zh-hans/dow…

Open R1 项目进展第一期

DeepSeek R1 发布已经两周了,而我们启动 open-r1 项目——试图补齐它缺失的训练流程和合成数据——也才过了一周。这篇文章简单聊聊:Open-R1 在模仿 DeepSeek-R1 流程和数据方面的进展 我们对 DeepSeek-R1 的认识和相关讨论 DeepSeek-R1 发布后社区搞出来的有趣项目这既是项目…

GPU内核实现(下)

3. ELLPACK 内核 ELLPACK SpMV实现沿行并行计算。由于数据已被重新排序为以列为主存储,因此沿ELLPACK数据连续行的内存访问被合并。在下面显示的实现中,假设输入cols和vals数组已经转换为ELLPACK格式。这种格式的一个关键部分是元数据参数,即每行非零的最大数量,它也作为参…

GPU内核实现(上)

GPU内核实现 以下是基于CSR和ELLPACK格式的一些标准SpMV实现。 1. 标量CSR内核 GPU加速SpMV的最简单实现之一是标量内核方法。标量内核分配一个线程来处理SpMV中的每个稀疏点积。稀疏点积由每个线程以顺序方式处理,从而消除了对需要共享内存和/或扭曲级别降低的更高级技术的需…

稀疏矩阵向量乘法介绍

稀疏矩阵向量乘法介绍 稀疏矩阵向量乘法(SpMV)是每个隐式稀疏线性代数求解器。从简单的 Krylov 算法到 multigrid 的算法性能方法在很大程度上取决于 SpMV 实现的速度。因为 SpMV 具有非常低的算术强度,定义为浮点操作数,则实现速度受内存带宽。最大化内存带宽的实现将实现…