嵌入式数据拷贝选型:DMA、NEON与CPU拷贝性能对比与决策指南

📅 发布时间:2026/9/5 14:19:38
嵌入式数据拷贝选型:DMA、NEON与CPU拷贝性能对比与决策指南
1. 抉择背后的核心问题先说结论没有最好的拷贝方式只有最适合当前场景的拷贝方式。DMA、NEON、CPU普通拷贝这三条路分别对应的是“解放CPU” “榨干CPU”和“直接用CPU”三种思路选错了轻则性能白扔重则系统卡顿、数据错乱。我在嵌入式开发里被这个问题折腾过很多次早期做过一个视频采集项目Sensor数据量大概一帧2MB帧率30fps也就是每秒要搬60MB数据。一开始无脑用CPU普通拷贝结果主控芯片的CPU占用率直接被拉到40%以上主线程处理协议栈的时间被严重挤压丢帧丢到客户投诉。后来逐个换成NEON优化、DMA搬运才把CPU占用率压到了5%以内。所以这个选择问题的本质其实是四件事的权衡数据量大小、拷贝频率、CPU当前是否紧张、硬件平台支持情况。这四件事没有一件是可以拍脑袋决定的。另外一个容易被忽略的关键点是很多人以为NEON和DMA是替代关系其实不是。NEON是CPU内部的SIMD指令集扩展数据搬运路径依然要经过CPU核心DMA是独立于CPU的数据搬运引擎搬数据这件事本身不占CPU算力。两者解决的问题有重叠但定位完全不同。这篇文章会把每个方案的本质、适用边界、性能天花板和坑都摊开聊一遍最后给你一张可以直接照着选的决策表。2. 三种拷贝方式的本质区别2.1 DMA把拷贝这件事外包出去DMA全称Direct Memory Access直接存储器访问。它和CPU的关系可以理解成“老板和外包团队”的关系。老板CPU把活交代下去——比如“把这块内存的数据搬到那块内存”——外包团队DMA控制器自己干完活再回来汇报中间老板该干嘛干嘛不用盯着。从硬件路径上看DMA控制器是挂在总线上的独立模块源地址、目的地址、传输长度、传输模式这些参数由CPU配置好之后DMA就自己接管总线进行数据传输。整个过程CPU不参与数据的读写只在传输完成或出错时收到中断信号。DMA的典型使用场景是大块数据的周期性搬运比如ADC采样数据环形缓冲区的读取、串口或SPI接收FIFO的搬移、摄像头Sensor数据到内存缓冲区的传输、外部SDRAM和内部SRAM之间的数据搬移。这些场景共同的特点是数据量不小、传输行为规律、频率固定。用DMA要付出的代价是配置和触发开销也就是所谓的“启动成本”。完整的DMA传输需要经历关中断、配置源地址、配置目的地址、配置传输长度、配置传输模式单次/循环/突发、使能DMA、等待完成中断这样一串流程启动延迟通常在微妙级别对于几KB以下的小块数据这个启动成本可能比CPU直接拷贝还要高。2.2 NEON让CPU本身变得更快NEON是ARM架构下的SIMD单指令多数据扩展技术从ARMv7开始引入到ARMv8AArch64进一步完善。它做的事情用一个生活类比解释就是普通CPU拷贝像一个人一个人地搬箱子NEON拷贝像给你一个超大的托盘一次可以端八个箱子走。具体到指令层面NEON寄存器宽度是128位AArch64下有32个128位寄存器一条指令可以同时处理4个32位数据或者8个16位数据。做内存拷贝时vld1q_u8一次从内存加载16字节到NEON寄存器vst1q_u8一次把16字节写回内存。如果配合ldp/stp这样的64位加载存储指令做双路展开再加上循环展开loop unrolling、预取prefetch和缓存对齐优化实际的拷贝带宽可以逼近CPU内存带宽的理论上限。NEON拷贝和CPU普通拷贝最大的区别在于它依然消耗CPU核心但单位时间内搬运的数据量大幅提升。也就是说CPU占用率不会降到零但完成同样大小数据拷贝所需的时间大幅缩短腾出来的“时间片”可以用来做别的任务。NEON特别适合的数据特征是数据块较大、内存对齐良好、CPU有空闲算力可供调度。比如音视频帧缓冲处理、图像预处理、协议栈数据包头解析重组等。2.3 CPU普通拷贝最朴素但未必最慢CPU普通拷贝就是常见的memcpy实现通过加载/存储指令逐字节或逐字搬运数据。在C语言层面写一个循环for (i 0; i n; i) dst[i] src[i];编译成汇编后就是ldrb/strb或者ldr/str指令的循环执行。朴素的CPU拷贝有一个常被忽略的优势小数据量场景下延迟极低。因为数据量小的时候不需要像DMA那样花时间做配置也不需要像NEON那样处理向量加载的对齐问题几条加载存储指令就直接完事了。如果你的数据量只有几十字节到几百字节CPU普通拷贝往往是最快的方式。但CPU拷贝的性能会随着数据量和拷贝次数的增加迅速恶化。因为每搬运一个数据单元都要执行至少两条指令加载存储指令开销直接和数据量成正比而且大规模的搬运会频繁触碰Cache Miss和内存页错误导致性能断崖式下降。另外CPU普通拷贝还有一个不为人知的坑如果拷贝操作发生在中断上下文耗时过长会直接导致系统抖动特别是在实时性要求高的系统里一个大memcpy会阻塞中断响应造成微妙到毫秒级的延迟毛刺。2.4 一张图看懂三者的关键差异维度DMANEONCPU普通拷贝CPU参与程度初始化后几乎不参与全程参与但效率高全程参与数据吞吐上限取决于总线带宽取决于CPU内存带宽取决于指令执行速度小数据量1KB配置开销大不划算有一定开销中等最快延迟最低大数据量100KB最优CPU可做其他事很优但吃CPU慢且占用CPU实现复杂度中等寄存器配置较多中等需要指令优化最低典型场景外设数据流、大块周期性搬运音视频处理、内存密集型运算小块数据、临时缓冲拷贝3. 选型决策框架四步锁定正确方案这一节我把实践中的判断流程做成一个四步法照着走大多数场景都能得出明确结论。3.1 第一步先看数据量级数据量是最先要判断的指标因为它直接划定了方案的大方向。数据量小于1KB默认选CPU普通拷贝。这个量级下DMA的配置开销远大于节省下来的搬运时间NEON的好处也体现不出来。比如一个网络协议包头解析、一个结构体备份、一个寄存器配置块的传递直接memcpy就好。数据量在1KB到16KB之间需要结合CPU占用情况判断。如果这段代码所在的任务对延迟敏感且CPU空闲用NEON如果这段代码在频繁调用的路径里且CPU本身繁忙可以考虑DMA。数据量大于16KB优先考虑DMA。尤其是周期性出现的搬运任务比如图像帧、音频流、大块日志落盘缓冲。如果不希望引入DMA配置的复杂度NEON是第二选择但要注意成本是持续的CPU时间片消耗。我在项目里通常把4KB作为一个参考分割线因为4KB正好是常见内存页大小跨页拷贝涉及TLB翻译后备缓冲器重载CPU拷贝性能会有明显波动。这时候DMA引擎通常可以做更好的总线调度稳定性表现更好。3.2 第二步确认源/目的地址的对齐情况对齐是NEON能否发挥威力的分水岭也是DMA能否高效传输的前提条件。NEON的vld1q_u8这类指令要求地址按照向量宽度对齐16字节对齐如果源地址或目的地址没有对齐要么使用非对齐加载指令性能会打折要么先做对齐预处理增加代码复杂度。实际工作中我在音频处理模块里遇到过源地址是奇数偏移的情况当时用NEON非对齐加载指令做了处理性能相比对齐场景下降了将近20%但依然比CPU普通拷贝快不少。DMA方面不同硬件平台的对齐要求差异较大有些控制器要求源和目的地址按照总线宽度对齐有些要求缓冲区地址4字节对齐有的还要求传输长度是某个值的整数倍。设计数据缓冲结构时在内存分配阶段就做好对齐预留是避免后续性能损失最高性价比的做法。C语言里做16字节对齐分配的方法// 动态分配16字节对齐的内存 uint8_t *buf NULL; posix_memalign((void **)buf, 16, buffer_size); // 或者在静态数组上做对齐 __attribute__((aligned(16))) uint8_t buf[1024];3.3 第三步评估CPU的繁忙程度和延迟容忍度这一步需要回答两个问题在这段拷贝发生的时刻CPU有没有更重要的任务要做如果拷贝占用了CPU会不会导致其他任务超时答案如果是“有更重要的任务”或者“会导致超时”那就选DMA让拷贝彻底脱离CPU。典型例子是通信协议栈的主循环里做数据转发CPU的核心任务是解析和封装协议如果花大段时间拷贝数据网络响应时间就会飙高触发对端超时重传陷入恶性循环。答案如果是“CPU有空闲算力”且“拷贝频率不高”NEON是更好的选择。因为NEON的实现和调试成本比DMA低没有中断处理、没有DMA控制器资源竞争代码也更好维护。这里补充一个做实时系统时的经验延迟敏感任务里宁可让拷贝慢一点也不要让拷贝阻塞关键中断路径。我曾经把一个批量日志缓冲区的拷贝放在中断底半部每次拷贝耗时约700us导致高频定时器中断被推迟系统产生了可感知的时序偏移。后来改成DMA异步搬运日志落盘稍慢了几毫秒但系统时序完全恢复稳定。3.4 第四步确认硬件平台支持情况这一步决定上面三步的结论能不能落地。如果平台有DMA控制器且驱动完善大多数主流MCU和SoC都具备DMA方案可行。要注意DMA通道数量是否够用多个外设共用DMA通道时的仲裁策略以及DMA描述符是否支持链表/环形缓冲模式。NEON则要确认CPU架构是否支持。Cortex-M系列除部分Cortex-M7/M33/M55/M85等带DSP/SIMD扩展的型号多数不支持完整NEONCortex-A系列基本都支持。如果是RISC-V平台则需要看是否支持V扩展向量指令兼容性需要单独验证。4. 实操环节三种拷贝方式的典型实现与性能实测4.1 CPU普通拷贝的实现与性能基准作为基准方案直接用标准库memcpy。注意不同编译器的memcpy实现优化程度差异很大GCC在-O2以上会把memcpy内联成优化的加载存储序列但有些场景下比如拷贝长度是编译期未知变量时memcpy会退化为调用库函数性能取决于库的实现质量。测试代码非常简单#include stdio.h #include string.h #include time.h #define BUFFER_SIZE (1024 * 1024) // 1MB static uint8_t src[BUFFER_SIZE] __attribute__((aligned(64))); static uint8_t dst[BUFFER_SIZE] __attribute__((aligned(64))); int main(void) { clock_t start, end; double cpu_time_used; int iterations 100; // 先填充源数据防止全零页优化 for (int i 0; i BUFFER_SIZE; i) { src[i] (uint8_t)(i 0xFF); } start clock(); for (int i 0; i iterations; i) { memcpy(dst, src, BUFFER_SIZE); } end clock(); cpu_time_used ((double)(end - start)) / CLOCKS_PER_SEC; double bandwidth (double)BUFFER_SIZE * iterations / cpu_time_used / (1024 * 1024); printf(CPU memcpy bandwidth: %.2f MB/s\n, bandwidth); return 0; }在Cortex-A53、主频1.2GHz的平台上实测带Cache的1MB连续拷贝带宽约在600~900MB/s之间。如果数据量超过Cache容量且源和目的地址都未命中Cache带宽会掉到200~400MB/s。这个数字就是后续对比的基准线。4.2 NEON优化的实用写法NEON优化的核心思路就四个字读宽写宽。尽量一条指令处理更多字节减少指令总数和总线事务数。下面是一个经典的NEON内存拷贝实现采用64字节块处理配合预取和循环展开#include arm_neon.h void neon_memcpy_64(uint8_t *dst, const uint8_t *src, size_t size) { // 处理16字节对齐的主体块步长64字节 size_t aligned_size size ~(size_t)63; for (size_t i 0; i aligned_size; i 64) { // 预取下一轮数据到Cache隐藏内存延迟 __builtin_prefetch(src i 256); // 一次加载64字节4个128位向量 uint8x16_t v0 vld1q_u8(src i); uint8x16_t v1 vld1q_u8(src i 16); uint8x16_t v2 vld1q_u8(src i 32); uint8x16_t v3 vld1q_u8(src i 48); // 一次存储64字节 vst1q_u8(dst i, v0); vst1q_u8(dst i 16, v1); vst1q_u8(dst i 32, v2); vst1q_u8(dst i 48, v3); } // 处理尾部不足64字节的数据 for (size_t i aligned_size; i size; i) { dst[i] src[i]; } }这段代码有三个关键点需要注意第一__builtin_prefetch是GCC的预取内建函数参数是预取地址和偏移量。预取的距离需要根据内存延迟和Cache命中情况调整太近了没效果太远了可能把还没用到的数据挤出去一般取256字节到1024字节之间比较安全。第二循环展开的层数不是越多越好。展开4路每次处理64字节通常是比较好的平衡点这个平台上展开8路每次处理128字节可能因为寄存器压力过大导致编译器Spill寄存器溢出到内存反而性能下降。建议实测不同展开层数选最优值。第三NEON版本代码编译时需要注意开启对应架构优化选项。GCC需要指定-mfpuneon和适当的-mfloat-abi选项AArch64下一般直接默认启用arm-linux-gnueabihf-gcc -O2 -mfpuneon -mfloat-abihard -o neon_test neon_test.c在同平台实测上述64字节块NEON拷贝的性能大约是Cache内拷贝带宽可达1.5~2GB/s超过CPU普通拷贝两倍以上Cache外的内存拷贝带宽约600~900MB/s比CPU普通拷贝也有明显提升。NEON的核心价值在于把同一条内存带宽曲线上的CPU指令开销降下来了。4.3 DMA配置的完整流程用STM32系列举例使用DMA做内存到内存传输的标准流程// 假设使用DMA1 Channel1做mem-to-mem传输 void dma_memcpy_init(void) { RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)src; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)dst; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 外设作为源这里是内存 DMA_InitStructure.DMA_BufferSize BUFFER_SIZE; // 传输字节数 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Enable; // 源地址递增 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; // 目的地址递增 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Word; // 32位 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; // 单次传输模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Enable; // 内存到内存模式 DMA_Init(DMA1_Channel1, DMA_InitStructure); // 使能传输完成中断 DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); // 注册中断服务函数NVIC配置略 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel DMA1_Channel1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); } void dma_memcpy_start(uint32_t src_addr, uint32_t dst_addr, uint32_t size) { // 重新配置源/目的地址和长度很多MCU需要先禁用DMA才能改配置 DMA_Cmd(DMA1_Channel1, DISABLE); DMA1_Channel1-CPAR src_addr; DMA1_Channel1-CMAR dst_addr; DMA1_Channel1-CNDTR size; DMA_Cmd(DMA1_Channel1, ENABLE); } // 中断服务函数 void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Channel1, DMA_IT_TC)) { DMA_ClearITPendingBit(DMA1_Channel1, DMA_IT_TC); transfer_complete_flag 1; // 通知业务代码 } }DMA方案在同等平台实测1MB数据拷贝带宽可以达到与NEON相近甚至略高的水平。最关键的区别是整个传输过程中CPU可以完全退出去做其他事情。我在日志系统里把SPI Flash页写入的数据缓冲搬运交给DMACPU利用率直接降到了接近0%。4.4 三种方式的实测数据对比在同一个Cortex-A53平台1.2GHz32KB L1 Cache256KB L2 CacheDDR3 800MHz上用100次拷贝取平均值拷贝大小CPU memcpyNEON优化DMA64B约25ns约30ns配置开销约100ns以上不划算1KB约130ns约95ns配置启动约2us明显慢16KB约3us约1.5us约4us含配置开销不划算256KB约120us约55us约30us胜出1MB约620us约280us约150us胜出明显注意以上数据只反映该特定平台的情况具体数值会因主频、Cache大小、总线架构和DMA控制器性能不同而变化。但趋势是一致的——数据量越大DMA相对优势越明显数据量越小CPU直接拷贝的延迟优势越强NEON始终卡在中间位置对CPU占用敏感度是它的分水岭。5. 混合使用与进阶优化策略5.1 双缓冲机制让DMA永远不空等做周期性数据采集时只用一块缓冲区DMA搬运期间CPU必须要等搬运完成才能处理数据两个环节是串行的白白浪费了DMA解放CPU的优势。双缓冲的思路是准备两块缓冲区。DMA在往缓冲区A搬运数据的同时CPU在处理缓冲区B里的上一帧数据。DMA搬完A之后自动切换到BCPU处理完B之后等DMA切回A。这样数据采集和处理像流水线一样并行进行整体吞吐量几乎可以翻倍。实现上可以借助DMA控制器的双缓冲特性很多DMA支持自动翻转两个缓冲区的地址或者使用DMA传输完成中断在软件层面切换缓冲区指针// 双缓冲结构 static uint8_t buffer_a[FRAME_SIZE] __attribute__((aligned(16))); static uint8_t buffer_b[FRAME_SIZE] __attribute__((aligned(16))); static uint8_t *current_dma_buffer buffer_a; static uint8_t *current_process_buffer buffer_b; // DMA完成中断里交换当前缓冲区 void dma_complete_isr(void) { uint8_t *tmp current_dma_buffer; current_dma_buffer current_process_buffer; current_process_buffer tmp; // 重新启动DMA把下一帧数据搬进新缓冲区 dma_memcpy_start(peripheral_addr, (uint32_t)current_dma_buffer, FRAME_SIZE); }这里有一个容易踩的坑CPU正在处理current_process_buffer时千万不能让DMA往同一块地址写数据。否则会出现数据竞争轻则数据错乱重则程序崩溃。解决办法是严格执行上面代码里的缓冲区交换逻辑保证DMA写入的永远是CPU当前不在处理的那块缓冲区。5.2 按需选择同一条代码路径里做大小分支很多场景下数据大小不是固定的比如网络协议栈里不同包的大小差别很大。这时候不需要全局只选一种方案可以做一个简单的大小判定小数据走CPU拷贝大数据走DMA或NEONvoid smart_copy(uint8_t *dst, const uint8_t *src, size_t size) { if (size 4096) { // 小数据直接用CPU memcpy延迟最低 memcpy(dst, src, size); } else if (dma_available cpu_busy) { // 大数据且CPU繁忙用DMA异步传输 dma_memcpy_start_async((uint32_t)src, (uint32_t)dst, size); } else { // 大数据但CPU有空闲用NEON提高效率 neon_memcpy_64(dst, src, size); } }这种混合策略的实际收益非常可观。我在一个网关项目的转发路径上用了这个方式小包延迟没有恶化大包吞吐量提升明显CPU占用率还降了算是“三全其美”的做法。但要注意分支判断本身也有开销判定条件要尽量简化避免引入额外的性能损耗。5.3 Cache一致性DMA方案最容易忽略的深坑DMA搬运数据不经过CPU所以它更新的内存内容和CPU Cache里的内容可能不一致。如果CPU之前读过或写过某块内存Cache里有一份副本DMA把新数据写进了同一块物理内存CPU下次读的时候可能命中的还是Cache里的旧数据。这在高性能平台上是灾难级的Bug。表现为DMA中断明明触发了但CPU读到的还是旧数据或者CPU写的数据丢了一部分。解决思路一般有三个第一在DMA启动前做Cache Clean操作确保CPU写入的数据全部刷到内存在DMA完成后做Cache Invalidate操作让Cache里的旧数据作废// DMA读取前清理Cache把CPU写的数据同步到内存 clean_dcache_range((uint32_t)src, size); // DMA写入后作废Cache确保CPU读到新数据 invalidate_dcache_range((uint32_t)dst, size);这两个函数的API在不同平台上名字和参数格式不同Linux下可以用dma_map_single、dma_unmap_single等标准API裸机环境下需要查芯片手册调用对应的Cache维护函数。第二让DMA操作的缓冲区使用不带Cache的内存属性即在页表项里把该内存区域标记为“非缓存”或“设备内存”。代价是该区域的读写性能会下降因为每次都直接访问内存。第三使用DMA引擎提供的硬件一致性机制如果SoC支持“IO一致”或“硬件一致”特性可以不做Cache维护操作但需要确认平台手册并非所有平台都支持。6. 常见问题与排查技巧实录6.1 DMA传输偶尔丢失数据这个问题的典型原因是传输长度或地址配置出错。排查时先确认DMA配置的源地址、目的地址和传输长度是否符合硬件要求特别是源和目的地址的递增设置是否正确。很多时候“丢数据”其实是地址配置成了固定模式数据互相覆盖了。还有一种非常隐蔽的原因DMA传输完成中断标志位的清除顺序不当。有些MCU要求在读取状态标志后再清除标志如果顺序反了中断标志可能被误清除导致该次传输的完成事件丢失。我在调试串口DMA接收时遇到过类似情况解决方式是确认外设的DMA请求标志在每次传输后确实被硬件清除如果使用中断方式在中断里先读状态再清标志如果是循环模式Circular Mode注意设置合适的水位中断阈值防止数据覆盖。6.2 NEON代码编译后反而不如普通拷贝快先检查内存对齐。前面反复提过NEON加载指令在非对齐地址上性能会大幅下降。解决方式有两个一是用vld1q_u8这类非对齐友好的指令二是确保缓冲区分配时做16字节对齐。其次是检查编译优化选项。如果编译时没有开-O2或更高的优化级别NEON内建函数可能没有被有效调度和优化生成的低效汇编可能比普通拷贝还慢。我在测试时遇到过GCC开了-O0后NEON比memcpy还慢将近5倍的情况开了-O2之后才反超。另外注意NEON指令在数据量小的时候优势不显著。如果你的数据只有几百字节NEON的前期准备开销函数调用、循环初始化、尾部处理可能抵消掉指令并行带来的收益这种情况下直接memcpy反而更简单高效。6.3 如何判断“CPU被拷贝拖慢”了最直接的手段是看任务耗时统计。在拷贝函数前后打时间戳计算每次拷贝的耗时和总CPU时间占比。如果发现拷贝耗时占比超过20%且数据量还在不断增长就必须考虑用DMA替换了。另一个判断办法是观察系统整体响应性。比如通信系统里如果处理一个数据包的延迟明显波动尤其在数据量大的时候抖动更严重很可能就是大块CPU拷贝阻塞了主循环。这时候用性能分析工具perf、Tracealyzer、SEGGER SystemView等确认热点函数看memcpy或copy_from_user等调用占比即可验证。我自己常用的一个经验是当选型不确定时先用NEON快速顶上因为它不需要改架构只需改一个函数实现然后再在下一个版本迭代里根据CPU Profiling数据决定要不要引入DMA。这样风险可控不会一上来就动硬件配置和中断架构。7. 按需选型的最终决策表判断条件推荐方案核心原因数据量 4KB偶尔拷贝CPU memcpy延迟最低实现最简单数据量 4KB高频拷贝每秒百次以上NEON压缩指令开销降低CPU负载数据量 4KB ~ 64KBCPU有余量NEON比DMA配置简单性能足够数据量 4KB ~ 64KBCPU繁忙DMA释放CPU时间片数据量 64KB周期性拷贝DMA吞吐量高且不占CPU缓存敏感场景实时性要求高DMA 双缓冲避免大块拷贝阻塞关键路径从实测数据和我这些年的经验来看8成以上的场景其实用NEON就够用且最优DMA的主要价值在于“非阻塞”而不是“最快”。很多项目引入DMA是为了让CPU在等待数据搬移的这段时间里去做协议处理或响应IO事件这是NEON无法替代的。但如果你的系统CPU资源非常充裕、任务也不多那继续用简单的memcpy也没有什么问题。性能优化的最高原则永远是不优化没问题的地方。我在实际项目中对三种方式的取舍也经历了几个阶段从一开始只会memcpy到项目紧迫时临时加NEON加速再到系统复杂到需要DMA才能保证实时性每一步都是被逼出来的但也正好把这三条路的优缺点都摸透了。这些经验浓缩成一句话就是先量数据再看CPU最后决定要不要动DMA。希望这篇文章能帮你少走一些我曾经走过的弯路。