You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Linux下未初始化缓冲区拷贝速度远快于已初始化缓冲区问题咨询

问题:未初始化缓冲区拷贝速度远高于已初始化缓冲区

背景

我需要在配备32GB内存、运行4.15版本内核的X86-64架构Linux设备上开发测试软件,通过单个TCP socket生成100Gbps测试流量。测试环境为一对veth虚拟网卡,其中一块网卡位于独立netns网络命名空间中,带宽观测工具为bmon。

初始测试现象

我编写的初始测试代码(已移除部分合法性校验逻辑)如下:

void test(int sock) {
    int size = 500 * 0x100000;
    char *buff = malloc(size);
    //optional
    memset(buff, 0, size);
    int offset = 0;
    int chunkSize = 0x200000;
    while (1) {
        offset = 0;
        while (offset < size) {
            chunkSize = size - offset;
            if (chunkSize > CHUNK_SIZE) chunkSize = CHUNK_SIZE;
            send(sock, &buff[offset], chunkSize, 0);
            offset += chunkSize;
        }
    }
}

测试出现不符合直觉的结果:

  • 保留memset(buff, 0, size);初始化语句时,吞吐量仅约60Gbps
  • 移除该初始化语句后,吞吐量提升至约94Gbps

对照实验结果

我将全量初始化替换为按比例初始化逻辑:memset(buff, 0, size * ratio);,测试结果呈现明确线性关联:

  • ratio=0(完全不初始化):吞吐量最高,约94Gbps
  • ratio=0.5(初始化50%缓冲区):吞吐量约72Gbps
  • ratio=1.0(全量初始化):吞吐量最低,约60Gbps
    吞吐量随初始化比例升高线性下降。

复现代码与验证

我编写了排除网络栈干扰的纯拷贝测试代码,可稳定复现上述效应:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/stat.h>

int size = 500 * 0x100000;
char buf[0x200000];

double getTS() {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec + tv.tv_usec/1000000.0;
}

void test1(int init) {
    char *p = malloc(size);
    int offset = 0;
    if (init) memset(p, 0, size);
    double startTs = getTS();
    for (int i = 0; i < 100; i++) {
        offset = 0;
        while (offset < size) {
            memcpy(&buf[0], p+offset, 0x200000);
            offset += 0x200000;
        }
    }
    printf("took %f secs\n", getTS() - startTs);
}

int main(int argc, char *argv[]) {
    test1(argc > 1);
    return 0;
}

在测试机(Ubuntu 18.04、Linux 4.15内核、32GB内存)上的运行结果:

  • 不执行缓冲区初始化:总耗时1.35秒
  • 执行全量初始化:总耗时3.02秒

后续需求

后续需要将sendfile的性能优化到与calloc分配的全零缓冲区发包一致的水平,这是任务硬性要求,需要相关实现建议。

回答

底层原理解释

这个性能差异和TCP协议栈、网卡驱动没有关系,完全是Linux内存管理的两个核心机制导致的:匿名内存按需分配、全局零页共享映射。

  • 首先明确malloc的真实行为:它只分配进程虚拟地址空间的连续区间,不会立刻占用物理内存。只有当进程第一次读写对应虚拟地址、触发缺页异常时,内核才会给这段地址绑定实际的物理页。
  • 不做memset初始化时,malloc返回的所有虚拟地址,初始都映射到内核维护的同一个特殊物理页——全局零页。这是一个只读、内容全为0的共享物理页,所有刚申请还没写入过的匿名内存,默认都指向它。
    这时候不管是memcpy拷贝还是send发包读这块缓冲区,本质都是从这同一个共享物理页读数据。你申请的500MB缓冲区根本没有占用500MB物理内存,零页本身会一直留在CPU高速缓存里,整个读过程几乎没有内存带宽压力,速度自然极快。
  • 一旦调用memset写过缓冲区,就会触发写时复制(COW)机制:每一块被写入的虚拟地址,都会被内核分配独立的可写物理页,再把零页的内容复制过去,最后写入你设置的值(这里是0)。这时候500MB缓冲区是实打实占了500MB物理内存,拷贝/发包的时候需要从不同的物理页读真实数据,500MB的数据量远大于CPU缓存容量,内存总线带宽会成为瓶颈,速度自然大幅下降。
  • 测试到的性能随初始化比例线性下降也完全符合这个逻辑:初始化比例越高,被分配独立物理页的内存区域越大,拷贝时需要从物理内存读取的真实数据越多,性能损失就越明显,和比例呈线性关系。

补充说明:C标准里读取未初始化的malloc内存属于未定义行为,但在Linux平台上这个行为是确定的——首次申请未写入的堆内存读出来全是0,不会有随机值。很多人误以为未初始化内存都是随机垃圾值,那是栈内存的特征,堆内存因为零页映射机制不存在这个问题。

sendfile性能优化建议

现在不初始化缓冲区能跑到接近线速,本质是蹭了全局零页的优化,根本没有发生500MB级别的真实内存拷贝。要让sendfile达到同等性能,按以下方案实现即可:

  • 不要用磁盘上的实体文件作为sendfile源,改用memfd_create创建匿名内存文件,将其truncate到你需要的发送缓冲区大小后,不要对这个文件做任何写入操作。此时这个文件的页缓存全部映射到全局零页,sendfile读取时的路径和读未初始化malloc缓冲区完全一致,没有真实内存拷贝开销。
  • 确保网卡和veth设备开启GSO/TSO/GRO卸载能力,让sendfile走零拷贝路径时不需要内核做额外的分片拷贝。
  • 调整socket的发送缓冲区大小,和单次发送的chunk大小匹配,避免sendfile过程中出现内核态的额外数据拷贝。
  • 如果需要进一步压榨性能,可以搭配SO_ZEROCOPY套接字选项使用,但上述memfd+sendfile的方案已经能达到和未初始化用户态缓冲区发包一致的性能。

内容的提问来源于stack exchange,提问作者packetie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 18:22:20