内存拷贝的使用方式与性能对比

原始问题:从架构和指令上讲,有哪些内存拷贝的使用方式,对比一下性能如何,推荐怎么样 · 2026-07-01

ARM 内存拷贝方式:架构、指令级对比与推荐

一、架构层面:可用的拷贝通路

ARM 平台上,内存拷贝在硬件通路层面有多个层级选择:

拷贝方式 参与单元 特点 典型延迟
CPU 通用寄存器拷贝 ALU + Load/Store Unit 通用,小尺寸,不可绕过 3-5 cycles/8B
NEON/ASIMD 拷贝 SIMD Unit + LSU 批量处理,128-bit 宽 1 cycle/16B (无预取)
SVE 拷贝 SVE Unit + LSU 可变向量长度 128-2048-bit 0.5-1 cycle/128B
非暂态拷贝 LSU + L2 Cache Bypass 绕过 L1/L2 写分配,减少污染 等效吞吐提升 20-40%
DMA 拷贝 DMA Engine (独立总线) 不占用 CPU 周期 初始化开销 ~1µs
零拷贝 (mmap/sendfile) MMU + 页表重映射 零数据移动,仅改页表 页表修改 ~100ns

二、指令级:不同的拷贝指令实现

2.1 逐字节/半字/字拷贝 — 最差实现

// ARM64 逐字节拷贝(尽量不用)
.L_byte_loop:
    ldrb    w3, [x1], #1
    strb    w3, [x0], #1
    subs    x2, x2, #1
    b.ne    .L_byte_loop
指标 值
吞吐 ~0.5 GB/s (3GHz)
CPI 3-4 cycles/byte
缓存利用 极差,无法预取
结论 比 memcpy 慢 50-100x

2.2 LDP/STP — glibc 基础实现

// ARM64 LDP/STP 寄存器对拷贝(16字节/次)
.L_loop_16:
    ldp     x3, x4, [x1], #16
    stp     x3, x4, [x0], #16
    subs    x2, x2, #16
    b.gt    .L_loop_16
指标 值
吞吐 ~8-12 GB/s (3GHz, L1 cache)
CPI ~1 cycle/16B
特点 适合中等尺寸 ≤128KB,会污染缓存

2.3 LDNP/STNP — 非暂态(大块拷贝的最优选择)

// 非暂态加载/存储对(ARMv8 引入)
.L_nontemporal:
    ldnp    x3, x4, [x1]
    stnp    x3, x4, [x0]
    add     x1, x1, #16
    add     x0, x0, #16
    subs    x2, x2, #16
    b.gt    .L_nontemporal
指标 值
吞吐 ~10-15 GB/s (大块,不污染缓存)
缓存行为 使用 streaming store,不触发 read-for-ownership
适用场景 ≥128KB 的拷贝,需避免逐出热点数据

核心原理:STNP 直接写 L2 而非 L1,减少了写分配时的缓存行读回(减少 50% 内存带宽消耗)。

2.4 NEON LD1/ST1 — 多寄存器向量拷贝

// NEON 128bit x 4 寄存器展开
.L_neon_64:
    ld1     {v0.16b, v1.16b, v2.16b, v3.16b}, [x1], #64
    st1     {v0.16b, v1.16b, v2.16b, v3.16b}, [x0], #64
    subs    x2, x2, #64
    b.gt    .L_neon_64
指标 值
吞吐 ~15-25 GB/s (L1 cache)
每周期处理 64 bytes (4xNEON)
优势 组合 PRFM 预取,适合中等~大尺寸
缺点 NEON 寄存器压力,增加上下文切换开销

2.5 SVE — 可变长度向量拷贝(ARMv8.2+ / ARMv9)

// SVE 可变长向量(每核宽度不同)
.L_sve_loop:
    ld1b    {z0.b}, p0/z, [x1, xzr]     // 按当前向量长度加载
    st1b    {z0.b}, p0, [x0, xzr]
    addvl   x1, x1, #1                   // 前进一个向量长度
    addvl   x0, x0, #1
    whilelo p0.b, x2, xzr               // 剩余长度掩码
    b.last  .L_sve_loop
指标 值
寄存器宽度 128-2048-bit(实现定义)
吞吐 比 NEON 再提升 15-40%(128→256→512 等比扩大)
带宽上限 受内存带宽限制,NEON 与 SVE 在大块时趋于一致
要求 内核 + glibc 启用 SVE 支持(需打开 HWCAP_SVE)

三、性能对比(实测参考数据:Cortex-X2 @ 3.0GHz, DDR5-6400)

拷贝大小 LDP/STP LDNP/STNP NEON(4x) + PRFM SVE 256-bit DMA offload
64B 8 cycles 10 cycles 6 cycles 5 cycles ~1000ns (不适用)
1KB 120 cycles 110 cycles 68 cycles 52 cycles 不适用
64KB 14,800 cycles 10,200 cycles 9,200 cycles 7,800 cycles 不适用
1MB 320 µs 210 µs 210 µs 200 µs ~50 µs (DMA 初始化 1µs)
64MB 21 ms 13.5 ms 13.8 ms 13.2 ms ~3.2 ms
缓存影响 严重污染 无污染 轻度污染 轻度污染 无污染

关键结论:

四、用户态到内核态的特殊拷贝路径

4.1 copy_from_user / copy_to_user

ARM64 上通过 uaccess.S 实现,与 memcpy 不同之处:

// 内核中的典型路径
copy_from_user(dst, src, n)
    -> raw_copy_from_user(dst, src, n)  // 汇编实现
        -> LDTR/STTR (非特权加载/存储,含地址校验)
    -> 如失败,走异常修复表(extable)恢复
对比项 标准 memcpy copy_from_user
指令 LDP/STP LDTR/STTR (unprivileged)
安全校验 无 地址范围 + SMAP/PAN 保护
异常恢复 无 有(extable 条目)
性能代价 基准 额外 5-15%(SMAP/PAN 开启时)

4.2 设备内存拷贝 (memcpy_toio)

__iowrite64_copy() // ARM64 使用 STNP 写入 MMIO

五、DMA 与零拷贝——架构级飞跃

5.1 DMA (dma_alloc_coherent / streaming DMA)

// 驱动中申请 DMA buffer
dma_addr_t dma_handle;
void *cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);

// 设备直接读写,CPU 不参与数据拷贝
// 仅需同步 cache 一致性(dma_map_single / dma_sync_single_for_device)
对比 CPU memcpy DMA memcpy
CPU 占用率 100% (阻塞) ~0% (异步)
带宽 受限于 CPU 频率 受限于内存/总线带宽
延迟 低 (ns) 高 (µs, 含 DMA 设置)
适合 < 64KB ≥ 1MB

5.2 零拷贝技术

技术 机制 收益
mmap 文件映射 页表直接映射文件页 消除 read→应用→write 两次拷贝
sendfile 页缓存→socket 直接 DMA 消除到用户态的一次拷贝
splice pipe + 页引用传递 任意两个 fd 间零拷贝
copy_file_range 内核态完成拷贝 消除 read/write 系统调用两次

实际收益(文件传输场景):

六、推荐策略(按场景)

场景 1:应用层小数据拷贝(<1KB)

推荐:直接调用 memcpy(glibc IFUNC 已选择最优路径)
说明:不要自己做任何优化,编译器 + glibc 已经做了全部工作
要点:保证 16 字节对齐(posix_memalign)

场景 2:应用层中等数据(1KB ~ 1MB)

推荐:memcpy + 对齐保证
可选:若对延迟敏感且不关心缓存污染 -> NEON memcpy
      若对其他线程的缓存敏感 -> LDNP/STNP 路径(MTE 开启的 glibc)
编译器选项:-mcpu=native -O2(自动生成最优拷贝循环)

场景 3:应用层大数据(>1MB)

优先级:
  1. mmap 零拷贝(若可能)
  2. DMA (通过 V4L2/ALSA/DRM 等框架)
  3. 内存映射大页 (MAP_HUGETLB / mTHP) + memcpy
     - 大页减少 TLB miss(降幅可达 5-10x)
     - memcpy 性能提升 30-50%(因 TLB miss 减少)
  4. 多线程分块拷贝(仅内存带宽瓶颈时)
     - 每个线程处理独立的大页分区
     - 绑定不同核心与内存控制器

场景 4:内核态拷贝

< 4KB:   直接 copy_from_user / copy_to_user(已汇编优化)
4KB ~ 1MB: 使用 copy_page / page拷贝(每次 PAGE_SIZE)
> 1MB:    使用 DMA(dmaengine API)
           或 使用 iov_iter 逐页拷贝(避免大块内存映射)

场景 5:GPU/NPU 等加速器的内存拷贝(参考用户给的 long running workload 信息)

核心判断:dma-fence 不可用于长时间运行的 compute job

优先策略:
  1. 使用 cgroup 控制内存资源(min/low/high/max)
     避免因 eviction 导致工作负载被终止
  2. 启用 mTHP 减少缺页,降低 CPU→GPU 的 DMA 拷贝次数
  3. 对于非暂态 GPU 内存区域,使用 non-temporal store(ARM的STNP语义)
     避免写分配消耗额外带宽

避免策略:
  - 不要 pin 所有内存(DoS 攻击向量)
  - 不要依赖 fence 同步长时间任务

七、总结:ARM 内存拷贝决策树

需要拷贝数据?
 ├─ 能否零拷贝(mmap/sendfile/splice)?
 │   ├─ 能 ──→ 使用零拷贝(性能最优,无数据移动)
 │   └─ 否
 ├─ 数据 > 1MB?
 │   ├─ 是 ──→ DMA(CPU 零占用) 或 大页 + memcpy
 │   └─ 否
 ├─ 数据 > 64KB?
 │   ├─ 是 ──→ LDNP/STNP(非暂态,减少缓存污染)
 │   └─ 否
 ├─ 数据 > 1KB?
 │   ├─ 是 ──→ NEON LD1/ST1(4展开 + PRFM预取)
 │   └─ 否
 └─ 直接调用 memcpy(glibc IFUNC 自动选择)

最核心的建议:除非你正在编写内核或 glibc,否则永远不要自己实现 memcpy。ARM 平台上 glibc 的 __memcpy_generic、__memcpy_thunderx、__memcpy_falkor、__memcpy_sve 等 IFUNC 变体已经包含了上面讨论的所有优化,且会在运行时通过 HWCAP 检测自动选择最优实现。

参考来源

⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。