4、CPU与GPU协同:数据从CPU到GPU的搬运、DMA传输、内存映射(mmap)

好,咱们进入第四章。前面几章我们把双缓冲的框架搭起来了,也聊了刷新时序。但有个核心问题一直没细讲——数据到底是怎么从CPU跑到GPU显存里的?

你想想看,CPU和GPU是两个独立的处理器,各自有各自的内存空间。CPU用的是系统内存(DDR),GPU用的是显存(VRAM)。它们之间隔着一道PCIe总线,数据要跨过这道“天堑”才能完成传输。这中间的门道,比你想象的要深得多。

4.1 数据搬运的三种方式

我在项目中遇到过不少新手,上来就写memcpy把数据从CPU拷贝到GPU。跑起来倒是能跑,但性能惨不忍睹。为什么?因为memcpy走的是CPU,而CPU干这种批量搬运的活,效率其实很低。

说白了,数据从CPU到GPU,主要有三种方式:

方式 原理 延迟 带宽 CPU占用
CPU直接拷贝 CPU逐字节/逐块搬运 高 低 100%
DMA传输 DMA控制器独立搬运 中 高 几乎为0
内存映射(mmap) 共享物理内存页 低 极高 0%(零拷贝)

核心结论:能走DMA就别走CPU,能用mmap就别用DMA。这是内存优化的第一铁律。

4.2 CPU直接拷贝——最笨但最通用的方法

先说说最原始的方式。CPU直接拷贝,就是CPU从源地址读数据,写到目标地址。听起来简单,但问题在于:

  • CPU被占死:搬运期间CPU没法干别的,只能当搬运工。
  • 缓存污染:大批量数据会冲掉CPU L1/L2缓存里的有用数据。
  • 带宽瓶颈:CPU的内存控制器带宽有限,远不如DMA专用通道。

我早期做嵌入式图形开发时,就吃过这个亏。那时候用memcpy把帧缓冲数据从系统内存拷到显存,结果CPU占用率飙到90%以上,UI响应卡成PPT。后来一查,发现memcpy在搬运大块数据时,会触发大量的缓存未命中(cache miss),性能直接腰斩。

避坑指南:我曾经在项目中看到有人用for循环逐像素拷贝——千万别这么干!memcpy已经是最优的CPU拷贝实现了,逐像素拷贝性能会差两个数量级。

4.3 DMA传输——让硬件替你干活

DMA(Direct Memory Access)是什么?说白了,就是CPU把搬运任务外包给一个专门的硬件——DMA控制器。CPU只需要告诉DMA控制器:“从地址A搬N个字节到地址B”,然后就可以去干别的事了。DMA控制器自己完成搬运,搬完了发个中断通知CPU。

DMA传输的典型流程:

  1. CPU配置DMA描述符:设置源地址、目标地址、传输长度。
  2. CPU启动DMA:写一个寄存器,告诉DMA“开始干活”。
  3. DMA独立搬运:DMA控制器通过系统总线,直接从源地址读、写到目标地址。
  4. DMA完成中断:搬运结束,DMA触发中断,CPU处理后续逻辑。

嗯,这里要注意:DMA传输虽然解放了CPU,但它有个限制——源和目标必须在同一个物理地址空间内。CPU内存和GPU显存是两套独立的地址空间,所以DMA不能直接在它们之间搬运。那怎么办?

答案是:通过PCIe BAR(基地址寄存器)映射。GPU会把一部分显存映射到PCIe的地址空间里,这样DMA就能通过PCIe总线访问到显存了。这个过程对开发者是透明的,驱动层帮你搞定了。

个人经验:我建议在驱动开发时,优先使用DMA传输大块数据(比如整帧图像)。DMA的带宽利用率通常能达到PCIe理论带宽的80%以上,而CPU拷贝往往只有30%-50%。

4.4 内存映射(mmap)——零拷贝的终极方案

终于说到重点了。内存映射(mmap)是什么?它让CPU和GPU共享同一块物理内存,数据根本不需要“搬运”。

原理是这样的:

  • 操作系统把GPU显存(或一块共享内存)映射到CPU的虚拟地址空间中。
  • CPU可以直接读写这块内存,就像读写普通内存一样。
  • GPU也能直接访问同一块物理内存。
  • 数据不需要拷贝——CPU写进去,GPU直接读,反之亦然。

这就是所谓的零拷贝(Zero-Copy)技术。性能提升是质的飞跃。

为什么会这样?因为传统的DMA传输,数据至少要在系统内存和显存之间走一遍PCIe总线。而mmap让CPU和GPU共享同一块物理内存,省掉了PCIe传输的开销。

关键点:mmap不是所有场景都适用。它要求硬件支持统一内存访问(UMA)或共享虚拟内存(SVM)。在独立显卡(dGPU)上,mmap通常只能映射一小块显存,不能映射全部显存。

4.5 三种方式的性能对比

我拿一个实际项目的数据来说话。假设我们要传输一帧1920x1080的RGBA图像(约8MB),在PCIe 3.0 x16的平台上:

传输方式 延迟(微秒) 带宽(GB/s) CPU占用率 适用场景
CPU直接拷贝 ~500 ~2 95% 小数据量、临时调试
DMA传输 ~100 ~10 5% 大块数据、帧缓冲更新
mmap零拷贝 ~1 ~30+ 0% 高频更新、实时渲染

看到差距了吧?mmap的延迟只有DMA的1%,带宽是DMA的3倍以上。CPU占用率更是天壤之别。

避坑指南:我曾经在项目中用mmap映射了整块显存,结果发现CPU访问映射区域时,会触发缺页中断(page fault),导致性能反而下降。后来才明白——mmap映射的物理内存页必须提前锁定(mlock),否则操作系统可能会换出到磁盘,那就全完了。

4.6 知识体系总览

下面这张图把CPU与GPU协同的数据流梳理清楚了。你可以看到,从CPU到GPU有三条路径,每条路径的延迟、带宽和CPU占用都不一样。

CPU与GPU数据协同三种方式 CPU GPU CPU直接拷贝 延迟高 · CPU占用100% DMA传输 延迟中 · CPU占用≈0% mmap零拷贝 延迟极低 · CPU占用0% 共享物理内存(UMA/SVM) 说明: ① CPU直接拷贝:CPU逐字节搬运,适合小数据量,但会严重占用CPU。 ② DMA传输:DMA控制器独立搬运,CPU几乎零占用,适合大块数据。 ③ mmap零拷贝:CPU和GPU共享物理内存,无需搬运,性能最优。

4.7 实际项目中的选择策略

好了,三种方式都讲完了。那在实际项目中,到底该怎么选?我个人的经验是:

  • 小数据量(< 1MB):用CPU直接拷贝。DMA的配置开销可能比传输本身还大,不划算。
  • 大数据量(1MB ~ 100MB):用DMA传输。这是最均衡的方案,CPU几乎零占用,带宽也够用。
  • 高频更新(每帧都变):用mmap零拷贝。比如实时渲染的帧缓冲、视频播放的YUV数据,mmap能省掉每次的拷贝开销。
  • 硬件不支持mmap:那就只能用DMA了。别硬上mmap,否则缺页中断会让你怀疑人生。

个人习惯:我一般在项目初期先用DMA传输,保证功能稳定。等性能调优阶段,再针对热点路径换成mmap。这样既能快速出活,又能保证最终性能。