欢迎光临
我们一直在努力

工业视觉 C++ 后端的内存池、环形缓冲与零拷贝

1. "内存"拖停

光通信行业的 FAU(Fiber Array Unit,光纤阵列单元)与光纤线序检测,是典型的高分辨率 + 高帧率 + 7×24 连续运行场景。

  • 相机侧:通常使用 2000 万像素级面阵相机,或 8K/16K 线阵相机;
  • 节拍侧:一秒拍几张到几十张,单帧原始数据 20MB~120MB 不等;
  • 算法侧:需要做定位、端面检测、光纤排序(线序)识别,往往还要接一个推理引擎做缺陷分类。

算一笔账:2000 万像素 × 3 通道 × 8bit ≈ 60MB/帧,按 5 fps 计算,每秒就有 300MB 图像在内存里进出;线阵相机连续扫描时,瞬时带宽更高。

如果代码写得不严谨——cv::Mat 随手拷贝、new 出来的缓冲区没人 delete、回调里做深拷贝、推理前再 memcpy 一次——那么内存占用会随着运行时间单调上升。跑几个小时,进程 RSS 涨到几十 GB,最终 OOM 被内核 kill,产线停机。

这类问题的可怕之处在于:它不是"逻辑错误",测试环境跑十分钟完全正常,只有上了产线连续跑 8 小时才暴露。所以正确的做法不是"事后查泄漏",而是在设计阶段就把内存的生命周期管死。

本文讨论三个层层递进的设计点:

  • 内存池(Memory Pool):把"每帧 malloc/free"变成"启动时一次性分配、运行期只借还";
  • 环形缓冲区(Ring Buffer):用固定槽位 + 引用计数,实现图像的生灭管理,天然背压;
  • 零拷贝(Zero-copy):把相机 SDK 的内存指针直接喂给推理引擎,砍掉无意义的物理内存搬运。
  • 2.问题定位

    在动手设计之前,先把一条典型流水线上的拷贝点列清楚。很多"内存泄漏"其实不是泄漏,而是拷贝太多 + 释放太晚。

    #mermaid-svg-T96IbHiUFm8k2g4Q{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T96IbHiUFm8k2g4Q .error-icon{fill:#552222;}#mermaid-svg-T96IbHiUFm8k2g4Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T96IbHiUFm8k2g4Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T96IbHiUFm8k2g4Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T96IbHiUFm8k2g4Q .marker.cross{stroke:#333333;}#mermaid-svg-T96IbHiUFm8k2g4Q svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T96IbHiUFm8k2g4Q p{margin:0;}#mermaid-svg-T96IbHiUFm8k2g4Q .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-T96IbHiUFm8k2g4Q .cluster-label text{fill:#333;}#mermaid-svg-T96IbHiUFm8k2g4Q .cluster-label span{color:#333;}#mermaid-svg-T96IbHiUFm8k2g4Q .cluster-label span p{background-color:transparent;}#mermaid-svg-T96IbHiUFm8k2g4Q .label text,#mermaid-svg-T96IbHiUFm8k2g4Q span{fill:#333;color:#333;}#mermaid-svg-T96IbHiUFm8k2g4Q .node rect,#mermaid-svg-T96IbHiUFm8k2g4Q .node circle,#mermaid-svg-T96IbHiUFm8k2g4Q .node ellipse,#mermaid-svg-T96IbHiUFm8k2g4Q .node polygon,#mermaid-svg-T96IbHiUFm8k2g4Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-T96IbHiUFm8k2g4Q .rough-node .label text,#mermaid-svg-T96IbHiUFm8k2g4Q .node .label text,#mermaid-svg-T96IbHiUFm8k2g4Q .image-shape .label,#mermaid-svg-T96IbHiUFm8k2g4Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-T96IbHiUFm8k2g4Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-T96IbHiUFm8k2g4Q .rough-node .label,#mermaid-svg-T96IbHiUFm8k2g4Q .node .label,#mermaid-svg-T96IbHiUFm8k2g4Q .image-shape .label,#mermaid-svg-T96IbHiUFm8k2g4Q .icon-shape .label{text-align:center;}#mermaid-svg-T96IbHiUFm8k2g4Q .node.clickable{cursor:pointer;}#mermaid-svg-T96IbHiUFm8k2g4Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-T96IbHiUFm8k2g4Q .arrowheadPath{fill:#333333;}#mermaid-svg-T96IbHiUFm8k2g4Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-T96IbHiUFm8k2g4Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-T96IbHiUFm8k2g4Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T96IbHiUFm8k2g4Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-T96IbHiUFm8k2g4Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T96IbHiUFm8k2g4Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-T96IbHiUFm8k2g4Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-T96IbHiUFm8k2g4Q .cluster text{fill:#333;}#mermaid-svg-T96IbHiUFm8k2g4Q .cluster span{color:#333;}#mermaid-svg-T96IbHiUFm8k2g4Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-T96IbHiUFm8k2g4Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-T96IbHiUFm8k2g4Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-T96IbHiUFm8k2g4Q .icon-shape,#mermaid-svg-T96IbHiUFm8k2g4Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T96IbHiUFm8k2g4Q .icon-shape p,#mermaid-svg-T96IbHiUFm8k2g4Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-T96IbHiUFm8k2g4Q .icon-shape .label rect,#mermaid-svg-T96IbHiUFm8k2g4Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T96IbHiUFm8k2g4Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-T96IbHiUFm8k2g4Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-T96IbHiUFm8k2g4Q :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    ① SDK 内部缓冲

    ② 深拷贝到 cv::Mat

    ③ 队列出队再拷贝

    ④ 转 RGB / 归一化

    相机 SDK 回调

    用户回调 Buffer

    业务队列

    预处理/缩放

    推理引擎输入

    结果后处理

    上图中 ②③④ 每一处都是一次全帧拷贝。60MB 的帧,拷 3 次就是 180MB 的瞬时内存流量;如果队列里积压 20 帧,就是 1.2GB 常驻。

    设计目标:把 ②③④ 全部消除,让同一块物理内存在整条链路上"只被指针引用,不被复制"。

    3. 内存池设计

    3.1 核心思路

    工业视觉的图像尺寸是固定的(相机分辨率不变),这给了我们极大的便利:可以按"帧大小"预分配 N 个等长块,运行期只做借出/归还。

    具体做法是定义一个「帧池」对象,它在构造时接收两个参数:单帧字节数与池容量。构造阶段一次性把容量个等长缓冲区全部分配好,同时维护一个空闲槽位索引队列,初始时把所有槽位索引依次压入。

    对外只暴露三个动作:

    • 借出:加锁后从空闲队列头部取一个索引返回;若队列为空则返回 -1,表示池已耗尽,这是一个明确的背压信号;
    • 归还:加锁后把索引重新压回空闲队列尾部;
    • 取地址:根据索引返回该槽位缓冲区的首地址,以及单帧的字节长度。

    内部成员包括:单帧字节数、池容量、一个二维字节容器(外层是槽位、内层是帧数据,启动时一次性分配完毕)、一个空闲索引双端队列,以及一把互斥锁保护借还操作。

    要点:

    • storage_ 在构造时一次性 resize,运行期不再有任何堆分配,从根本上杜绝碎片与泄漏;
    • 用索引而不是裸指针作为句柄,避免悬垂指针;
    • acquire() 返回 -1 是背压信号,而不是抛异常——产线上宁可丢帧,也不能让内存无限增长。

    3.2 用 RAII 管住"归还"

    裸调 acquire/release 迟早会漏。用 shared_ptr 的自定义删除器把归还动作绑定到生命周期上:

    思路是定义一个「帧删除器」结构体,内部持有帧池指针与槽位索引,重载调用运算符,在被调用时执行「把该索引归还给帧池」。再基于它声明一个帧智能指针类型,即带自定义删除器的共享指针。

    随后封装一个「申请帧」函数:先向池借出索引,若返回 -1 则直接返回空指针,由调用方决定丢帧还是等待;否则用池中该槽位的地址加上删除器构造一个共享指针返回。

    这样,只要最后一个 FramePtr 析构,槽位自动回到池里。无论中间经过多少个队列、多少个线程,都不需要手工 release。

    注意:shared_ptr 的引用计数是原子操作,跨线程安全;但高频 acquire/release 时会有缓存行竞争,后续可用 per-thread 缓存或 std::atomic 无锁栈优化。

    4. 环形缓冲区:图像的生灭管理

    4.1 为什么是环形

    流水线是"生产者(相机回调)—消费者(算法线程)"模型。环形缓冲区天然适合:

    • 槽位数量固定,内存占用有上界;
    • 生产/消费用两个原子游标推进,无需加锁;
    • 满了就覆盖最旧帧或丢帧,永远不会 OOM。

    实现上是一个模板类,模板参数为元素类型与槽位数量(编译期常量)。内部有一个定长数组作为存储,以及两个原子整型游标:写游标(head)与读游标(tail)。

    • 入队:先以宽松内存序读取写游标,算出下一个位置(对槽位数取模);若下一个位置等于读游标(以获取内存序读取),说明队列已满,直接返回失败,由调用方决定丢帧还是覆盖最旧数据;否则把元素移动写入当前写游标位置,再以释放内存序更新写游标,返回成功。
    • 出队:先以宽松内存序读取读游标,若它等于写游标(以获取内存序读取),说明队列为空,返回失败;否则把该位置元素移动赋给输出参数,再以释放内存序推进读游标,返回成功。

    由于读写游标分属两个线程各自推进,且通过获取/释放语义建立先后关系,整个过程无需加锁。

    4.2 与内存池配合:槽位里放的是 FramePtr

    关键设计:环形缓冲区里存的不是图像数据,而是 FramePtr(智能指针)。

    声明时只需指定元素类型为帧智能指针、槽位数为 32,即得到一个「32 个槽位、只存指针」的帧队列。

    这样带来三个好处:

  • 队列本身极小(32 × 16 字节),拷贝成本可忽略;
  • 图像数据的生死由引用计数决定:只要还有消费者持有 FramePtr,槽位就不会被回收;
  • 生产者 push 失败时,说明下游处理不过来,此时丢帧并计数,而不是阻塞相机回调(阻塞回调会导致 SDK 内部缓冲堆积,反而更危险)。
  • #mermaid-svg-ZmPkT9M4Okd8wUYX{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZmPkT9M4Okd8wUYX .error-icon{fill:#552222;}#mermaid-svg-ZmPkT9M4Okd8wUYX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZmPkT9M4Okd8wUYX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .marker.cross{stroke:#333333;}#mermaid-svg-ZmPkT9M4Okd8wUYX svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZmPkT9M4Okd8wUYX p{margin:0;}#mermaid-svg-ZmPkT9M4Okd8wUYX .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .cluster-label text{fill:#333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .cluster-label span{color:#333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .cluster-label span p{background-color:transparent;}#mermaid-svg-ZmPkT9M4Okd8wUYX .label text,#mermaid-svg-ZmPkT9M4Okd8wUYX span{fill:#333;color:#333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .node rect,#mermaid-svg-ZmPkT9M4Okd8wUYX .node circle,#mermaid-svg-ZmPkT9M4Okd8wUYX .node ellipse,#mermaid-svg-ZmPkT9M4Okd8wUYX .node polygon,#mermaid-svg-ZmPkT9M4Okd8wUYX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZmPkT9M4Okd8wUYX .rough-node .label text,#mermaid-svg-ZmPkT9M4Okd8wUYX .node .label text,#mermaid-svg-ZmPkT9M4Okd8wUYX .image-shape .label,#mermaid-svg-ZmPkT9M4Okd8wUYX .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZmPkT9M4Okd8wUYX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZmPkT9M4Okd8wUYX .rough-node .label,#mermaid-svg-ZmPkT9M4Okd8wUYX .node .label,#mermaid-svg-ZmPkT9M4Okd8wUYX .image-shape .label,#mermaid-svg-ZmPkT9M4Okd8wUYX .icon-shape .label{text-align:center;}#mermaid-svg-ZmPkT9M4Okd8wUYX .node.clickable{cursor:pointer;}#mermaid-svg-ZmPkT9M4Okd8wUYX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .arrowheadPath{fill:#333333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZmPkT9M4Okd8wUYX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZmPkT9M4Okd8wUYX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZmPkT9M4Okd8wUYX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZmPkT9M4Okd8wUYX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZmPkT9M4Okd8wUYX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZmPkT9M4Okd8wUYX .cluster text{fill:#333;}#mermaid-svg-ZmPkT9M4Okd8wUYX .cluster span{color:#333;}#mermaid-svg-ZmPkT9M4Okd8wUYX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZmPkT9M4Okd8wUYX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZmPkT9M4Okd8wUYX rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZmPkT9M4Okd8wUYX .icon-shape,#mermaid-svg-ZmPkT9M4Okd8wUYX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZmPkT9M4Okd8wUYX .icon-shape p,#mermaid-svg-ZmPkT9M4Okd8wUYX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZmPkT9M4Okd8wUYX .icon-shape .label rect,#mermaid-svg-ZmPkT9M4Okd8wUYX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZmPkT9M4Okd8wUYX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZmPkT9M4Okd8wUYX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZmPkT9M4Okd8wUYX :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    成功

    满

    成功

    池满

    相机回调

    pool.acquire()

    SDK 数据写入槽位

    push 到 RingBuffer

    丢帧 + 计数告警

    算法线程 pop

    推理 / 检测

    FramePtr 析构

    槽位归还 pool

    4.3 生灭管理的三条铁律

    • 生产者永不阻塞:回调里只做 acquire + memcpy + push,任何耗时操作都丢给下游;
    • 消费者永不持有超过必要时间:处理完立刻让 FramePtr 出作用域;
    • 池容量 = 队列容量 + 在途帧数:留 2~3 帧余量给正在处理的帧,避免"队列没满但池空了"的假死。

    5. 零拷贝:把相机内存指针直接喂给推理引擎

    5.1 相机 SDK 的零拷贝接口

    主流工业相机 SDK(GigE Vision / USB3 Vision 厂商 SDK)都支持注册用户缓冲区:你把自己的内存地址交给 SDK,SDK 直接 DMA 写入,回调里拿到的是你自己的指针,而不是 SDK 内部缓冲。

    典型用法分两步:

  • 注册阶段:循环若干次(例如 8 次),每次先向帧池借出一个槽位索引,然后把该槽位的地址、单帧长度以及索引本身一起注册给相机,作为用户缓冲区。此后相机 DMA 会直接写入这些池内地址。
  • 回调阶段:相机 SDK 在回调中把数据指针、长度和用户索引回传。由于数据指针就是池中对应槽位的地址,回调里无需任何拷贝,直接用该地址与删除器构造一个帧智能指针,再推入帧队列即可。
  • 这一步直接消灭了第 2 节图中的 ① 和 ②:SDK 写的就是池里的内存,回调里拿到的也是池里的内存。

    5.2 推理引擎的零拷贝输入

    以 ONNX Runtime / TensorRT 为例,都支持"外部内存"输入:

    以 TensorRT 为例:先准备一个绑定指针数组,把输入设备指针与输出设备指针按顺序放进去,然后调用执行上下文的异步推理接口,把这条 CUDA 流交给它即可。整个过程中,输入数据始终停留在设备内存里,除了必要的 H2D 传输之外,不再产生任何额外的全帧拷贝。

    以 ONNX Runtime 为例:先创建一个描述内存属性的对象,指明使用 CPU 侧的内存分配器与默认内存类型;再用它把一块已经存在的内存(帧数据首地址 + 字节长度 + 维度信息)包装成一个张量值对象。这个包装动作只是登记指针与形状,并不会复制底层数据,因此推理引擎读到的就是帧池里的那块内存。

    配合 CUDA 的 cudaMemcpyAsync(H2D),整条链路只剩一次必要的设备传输,CPU 侧不再有任何全帧 memcpy。

    5.3 必须注意的坑

    • 对齐:相机 DMA 通常要求 4KB 对齐,池分配时用 aligned_alloc 或 cudaMallocHost(pinned memory);
    • 生命周期:推理是异步的,enqueue 返回不代表用完。必须等 stream 同步或 event 完成后再释放 FramePtr,否则会读到被覆盖的数据;
    • 格式转换:Bayer → RGB、归一化这些操作无法完全零拷贝,但可以在 GPU 上做,避免 CPU 侧再开一块缓冲;
    • pinned memory 有限:cudaMallocHost 分配过多会拖慢系统,池容量要按实际并发帧数设定。

    6. 完整流水线骨架

    把三块拼起来,一个可运行的最小骨架如下:

    主流程大致是这样组织的:

  • 参数与资源准备:先确定图像宽、高、通道数(例如 5472 × 3648 × 3),据此算出单帧字节数,约 60MB;然后构造帧池,容量取 16 帧,合计约 960MB,实际取值要按机器内存调整;再声明一个容量为 12 的帧队列,元素类型是帧智能指针。

  • 相机线程:在一个「运行中」标志控制的循环里反复执行——先向帧池借出一个槽位索引,若返回 -1 说明池已耗尽,直接把丢帧计数加一并进入下一轮;否则该槽位由相机 SDK 直接写入数据;随后用该槽位地址与删除器构造一个帧智能指针,尝试推入帧队列,若队列已满则同样把丢帧计数加一。

  • 算法线程:同样在循环里反复执行——从帧队列弹出一帧,若队列为空则让出 CPU 时间片后继续;拿到帧后把其裸指针直接交给推理函数,实现零拷贝输入;处理完毕显式重置该智能指针,使引用计数归零、槽位立即归还帧池。

  • 收尾:主线程等待相机线程与算法线程都结束后再退出。

  • 整个骨架里没有任何一处对整帧图像做深拷贝,内存的分配只发生在启动阶段,运行期只有借出与归还两种动作。

    7. 线上验证

    设计对不对,要用数据说话。建议在产线部署前做三件事:

  • 长稳测试:连续跑 24 小时,每 10 分钟采样 /proc/<pid>/status 的 VmRSS,画出曲线。合格标准是曲线走平,而不是缓慢上升。
  • 池水位监控:暴露 pool.free_count() 与 drop_count 指标,接入 Prometheus。空闲槽位长期为 0 说明容量不足或下游太慢。
  • ASan / Valgrind 抽测:在测试环境用 AddressSanitizer 跑一轮,确认没有 use-after-free(尤其是异步推理场景)。
  • 一个典型的健康曲线:启动后 RSS 稳定在 池容量 × 帧大小 + 固定开销,运行 24 小时波动不超过 5%。

    8. 总结

    FAU 与线序检测这类高吞吐工业视觉场景,内存管理的正确性直接等于产线的可用性。三个设计点层层递进:

    设计解决的问题关键手段
    内存池 频繁 malloc/free 导致的碎片与泄漏 启动时一次性分配,运行期借还
    环形缓冲区 队列无限增长导致 OOM 固定槽位 + 原子游标 + 丢帧背压
    零拷贝 多次全帧 memcpy 的带宽浪费 SDK 注册缓冲 + 引擎外部内存输入

    一句话概括:让每一帧图像从相机到推理,只被"引用",不被"复制";让每一块内存从分配到回收,都有确定的归属。

    做到这三点,系统跑 8 小时和跑 8 分钟,内存曲线应该是一模一样的。

    赞(0)
    未经允许不得转载:171主机测评 » 工业视觉 C++ 后端的内存池、环形缓冲与零拷贝
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址