Wii
Wii
发布于 2025-08-26 / 15 阅读
0
0

性能优化系列(一):零拷贝/少拷贝

前言

性能优化系列的初篇,起笔前有些犹豫,在大语言模型盛行的当下,再去写技术文章还有意义吗?想到一个很搞的理由,大模型没有经历过切肤之痛,写不出那种感同身受的感觉,哈。

最近这几年一直在做程序化广告的算法引擎,偏重在线部分,对高并发服务的性能和稳定性优化有一些经验。不管是汇量还是易点,流量仿佛取之不尽,用之不竭,这也暗示着三方广告平台面临的单条请求的价值较低的窘境,流量和机器成本成为一对矛盾体。

零拷贝/少拷贝

在广告算法引擎中,排序服务涉及大量的数据传输。这包括请求上下文、供给方数据、需求方数据、设备及行为数据等 Raw Data,需要对这些数据进行特征的抽取、归一化及计算,然后透传给推理服务。

在做成本优化时,通过 cpu profiler 发现在调用推理服务时,占用了较多的计算资源,细读推理服务的客户端代码,由于历史原因,存在两次请求数据格式的转换(C++ Object -> C++ Columnar Storage Struct -> Flatbuffer),在减少一次转换后,减少了 10% 的计算资源占用。

基于上面,零拷贝/少拷贝在部分大数据量、高并发服务中,可以节省较多的机器成本。

硬件层面

DMA(Direct Memory Access)

DMA(Direct Memory Access,直接内存访问)是一种允许外部设备(如硬盘、网卡、显卡等)直接与计算机内存进行数据传输,而无需中央处理器(CPU)全程干预的技术。它的核心作用是减轻 CPU 的负担,提高数据传输效率,尤其适用于大批量数据的快速读写场景。

这个技术已经很成熟了,就不过多赘述。

现在有一个 case,当 DMA 遇到虚拟化技术,会怎么样呢?宿主机是否会向虚拟机内存空间拷贝一份数据呢?

现在硬件虚拟化技术中,会用 IOMMU(IO 内存管理单元) 来解决这个问题。原理是,设备发起 DMA 时,IOMMU 拦截 DMA 地址,把它从 设备可见地址 → 宿主机物理地址(HPA) 做一次翻译。

目前主流的 IOMMU 实现包括:

  • Intel 的 VT-d(Virtualization Technology for Directed I/O)

  • AMD 的 AMD-Vi(I/O Virtualization Technology)

那怎么判断虚拟机是否使用了 IOMMU 技术呢?可以从宿主机和虚拟机两个角度查看 IOMMU 的支持情况。

查看宿主机是否支持

$ sudo dmesg | grep -e DMAR -e IOMMU

[    0.610359] pci 0000:00:00.2: AMD-Vi: IOMMU performance counters supported
[    0.616480] perf/amd_iommu: Detected AMD IOMMU #0 (2 banks, 4 counters/bank).

查看虚拟机是否开启 IOMMU

$ sudo dmesg | grep -e IOMMU -e VFIO -e PCI
...
[    0.689891] PCI-DMA: Using software bounce buffering for IO (SWIOTLB)

Using software bounce buffering for IO (SWIOTLB) 意思是当前环境里 DMA 没有用硬件 IOMMU,而是退化为软件模拟的 bounce buffer (SWIOTLB),此时虚拟机从设备读取的数据,会多一次内存拷贝。

操作系统层面

recvmsg

可以借助 recvmsg 等系统调用,减少从设备读取数据时的内存拷贝。

以网络数据传输为例,常规的 recv / read 会存在两次内存操作:

  1. 网卡通过 DMA 把数据写入内核的 socket buffer 里

  2. 用户代码调用 recv/read 系统函数时,操作系统会把 socket buffer 内的数据拷贝至用户态内存,期间存在内核态和用户态的切换

recvmsg 系统调用的优势:

  1. recvmsg 可以一次把 数据 + 元信息 (CMSG) 拿回来,而recvfrom + getsockopt 这种写法,可能需要两次系统调用

  2. recvmsg某些协议/场景 下,允许应用层 buffer 直接被网卡 DMA 发送,减少一次拷贝

从应用角度,folly 的 IOBuf 和 brpc 的 IOBuf 都有使用 recvmsg 来减少网络数据的内存拷贝。

mmap

借助 mmap 系统调用,可以避免 readmallocmemcpy 的多次拷贝。

read() 会把内核中已有的数据从内核缓冲区(page cache / socket buffer)拷贝到用户态缓冲区;而 mmap()把文件的页直接映射到进程地址空间,用户访问时内核只需将页(page)映射/填充到进程页表,不需要显式的 memcpy 把数据从内核区拷到用户区,避免一次拷贝(内核→用户的 memcpy)。

sendfile / splice / tee

sendfile 替换 send,sendfile 调用,内核直接把 page cache 的页面推送到 socket buffer,避免进入用户态,send 调用则会拷贝数据到内核的 socket buffer。

相似的函数还有 splice、tee 等。

框架层面

如上,folly 的 IOBuf 和 brpc 的 IOBuf 都有针对 IO 的内存拷贝优化,在关注性能的地方,可以尝试更换此类库。

代码层面

在这里梳理下 C++ 语言环境下,可以减少内存拷贝的实践。

  1. 使用 std::string_view 减少字符串数据的内存拷贝,注意数据的生命周期

  2. 返回值使用 const& 或 std::move 避免对象的拷贝,不过 C++ 17 后针对返回值有 RVO(Return Value Optimization) + move 的优化

  3. 借助共享指针,减少大对象的拷贝


评论