内存拷贝的使用方式与性能对比
原始问题:从架构和指令上讲,有哪些内存拷贝的使用方式,对比一下性能如何,推荐怎么样 · 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 |
| 缓存影响 |
严重污染 |
无污染 |
轻度污染 |
轻度污染 |
无污染 |
关键结论:
- < 1KB: NEON ± LDP/STP 最快,无显著差异
- 1KB ~ 64KB: LDNP/STNP 开始展示优势(减少缓存污染)
- > 1MB: LDNP/STNP 与 NEON 性能接近,DMA 更适合
- > 64MB: DMA 赢(固定 DMA 初始化开销 1µs 被摊薄),实际带宽不再受限于 CPU
四、用户态到内核态的特殊拷贝路径
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
- 必须使用非暂态指令(
STNP),避免缓存行合并
- 必须包含 DMB 屏障(顺序一致性)
- 吞吐受设备侧制约,典型 < 1 GB/s
五、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 系统调用两次 |
实际收益(文件传输场景):
- 标准 read+write: 两次 memcpy + 两次 syscall = 带宽 ~800 MB/s
- sendfile 零拷贝: 一次 DMA + 无 memcpy = 带宽 ~5 GB/s
六、推荐策略(按场景)
场景 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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。