ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CPU拷贝、NEON与DMA:嵌入式数据搬运选型指南

CPU拷贝、NEON与DMA:嵌入式数据搬运选型指南 1. 先说结论数据搬运这件事没那么简单做嵌入式开发和底层系统优化的朋友迟早都会遇到同一个问题一块数据要从A点挪到B点用CPU硬拷贝、用NEON做加速拷贝、还是交给DMA去后台搬运到底哪个更快哪个更省CPU哪个更适合我的场景说实话这个问题我在不同项目里被反复问过也踩过不少坑。早年间我写视频采集驱动一帧1080p的YUV数据大概3MB最开始用memcpy搬运结果CPU占用直接飙到40%多后来换成NEON优化降到了20%左右最后上了DMACPU占用几乎可以忽略不计。但有意思的是在某些小数据量的场景里DMA反而比CPU拷贝还慢。这里面的门道值得好好梳理一遍。这篇文章我会从原理、性能特征、代码实站和典型场景四个维度把这三种拷贝方式的底裤扒干净帮你建立一套适合自己的选型判断框架。文章涉及的代码以ARM Linux平台为主但思路对所有架构通用。2. 三种拷贝方式的底层原理剖析2.1 CPU拷贝最简单的“人工搬运”CPU拷贝就是我们最常见的memcpy操作。这个函数的本质很简单把源地址的数据逐字节或逐字、逐块读到CPU寄存器再写入目标地址。现代CPU做了很多优化来提速这个过程Cache预取硬件会自动预测即将访问的内存地址提前把数据加载到Cache里。对于顺序拷贝这种访问模式预取效果特别好。写合并Write CombiningCPU会把多个小的写操作合并成一个大的写操作减少总线事务次数。超标量与乱序执行现代CPU有多条执行流水线可以同时执行多个不相关的load/store指令。但在我们测试的场景里CPU拷贝有几个天然的瓶颈数据要先从内存到Cache再从Cache到寄存器路径很长而且CPU自己也要参与计算地址、控制循环。每条指令能搬的数据量有限。在ARM64上一条普通的LDR/STR指令只能搬8字节64位处理大块数据时指令数非常多。带Cache一致性维护的开销如果用DMA/设备写入的数据要先做Cache失效clean/invalidate才能保证读到的是新数据。从实测来看CPU拷贝的典型吞吐量取决于内存带宽。以DDR4-2400双通道为例理论带宽约38.4GB/s但memcpy实际能跑到的大概是10~15GB/s瓶颈主要在CPU处理指令的速率。2.2 NEON拷贝用SIMD做“流水线传送带”NEON是ARM的SIMDSingle Instruction Multiple Data扩展指令集。它的核心思想是一条指令操作多个数据。ARM64下NEON寄存器有32个每个128位宽一条VLD1指令就能搬16字节比普通的LDR多一倍。NEON拷贝的基本思路是用VLD1把一大块数据从内存加载到NEON寄存器组。处理完或者不需要处理再用VST1写回内存。由于NEON可以同时加载多个寄存器比如VLD1 {V0.16B, V1.16B, V2.16B, V3.16B}一口气加载64字节指令数大幅减少搬运效率显著提升。但从优化工程师的角度看这里最关键的并不是指令集本身而是几个配合点Cache预取指令PRFMNEON代码里通常会配合PRFM指令提前预取接下来的数据把访存延迟隐藏掉。数据对齐如果源地址和目标地址都是64字节对齐Cache Line对齐NEON搬运效率最高。不对齐时有的实现会先用标量代码处理头尾再把中间大块交给NEON。实测下来在ARM Cortex-A72这样的核上NEON拷贝对纯CPU memcpy的提速通常是1.5~2.5倍。但它的天花板依然是CPU访存指令的发射带宽所以本质还是CPU在搬只是搬得更高效。关于NEON还有一个值得关注的点ARM新一代的SVEScalable Vector Extension可伸缩向量扩展指令集向量寄存器宽度是可变的设计从128位到2048位都可以支持。SVE2在近年发布进一步丰富了数据变换指令。如果做的产品最终跑在支持SVE的处理器比如服务器CPU、新一代移动SoC的大核上可以考虑后续把NEON实现迁到SVE上代码里对向量长度做参数化处理便于适配不同硬件。2.3 DMA拷贝真正的“甩手掌柜”DMADirect Memory Access的思路是绕开CPU让数据在内存和外设之间、或者内存和内存之间直接搬运。一个完整的DMA传输流程是这样的驱动程序把源地址、目标地址、传输长度配到DMA控制器的寄存器里。发起传输请求DMA控制器自己接管总线按配置搬数据。搬完后通过中断通知CPU“我完事了”或者CPU轮询状态寄存器确认完成。这里有一点必须先说清楚DMA适合的是“CPU不该做的拷贝”。如果是内核态到内核态的纯内存拷贝DMA并不能直接参与因为DMA通常需要物理地址、要保证缓冲区在传输期间不被换页。你在用户态用memcpy就是最快的方式没法用DMA加速。真正适合DMA的场景是需要把数据从外设网卡、存储控制器、摄像头搬到内存或从内存搬到外设或者大块数据跨内存拷贝且对CPU占用敏感。典型如网络报文收发、SPI/I2C/UART数据收发、视频帧搬运等。以一个实际案例感受一下差异我们用DMA在两个内存缓冲区之间搬运64KB数据DMA控制器需要配置描述符、启动、等待中断完成整个流程硬件上跑得非常快带宽可以打到内存带宽的80%以上但CPU参与配置和中断处理的软件开销是固定的。2.4 关键对比硬核参数对照表先放一张对比表把三种方式的核心差异直接晾出来对比维度CPU memcpyNEON加速拷贝DMA拷贝指令数/字节高低几乎为零CPU占用率高中极低只有配置和中断开销带宽典型10~15GB/s20~30GB/s接近内存带宽延迟低低有配置启动延迟适合数据量4KB4KB~64KB64KB或持续传输实时性保证好好取决于DMA中断延迟/轮询周期复杂度最低中较高驱动/描述符/生命周期管理适用硬件所有平台ARMv7及以上带NEON几乎所有SoC都带DMA控制器这张表里的带宽数据是工程上的近似值不同SoC差异很大但趋势是一致的。3. 性能实测用数据说服自己3.1 测试环境与测试方法我先交代一下测试环境方便你复现和对比硬件平台基于Cortex-A72的4核SoC主频1.8GHzDDR4内存。软件平台ARM64 Linux 5.10内核。工具和方式用户态测试代码用C语言实现时间统计用clock_gettime每次测试跑1000次取平均。内核态DMA用了SoC厂商提供的DMA驱动API。测试的数据块大小分为1KB、4KB、16KB、64KB、256KB、1MB、4MB这几个档位覆盖从“小”到“大”的典型场景。3.2 实测数据数据大小CPU memcpy耗时NEON耗时DMA耗时含配置1KB0.6us0.4us3.1us4KB2.2us1.3us4.2us16KB8.4us4.6us5.8us64KB32.2us17.1us12.4us256KB128us66us34us1MB512us260us113us4MB2060us1050us430us从数据能清楚地看出一个趋势1KB以下CPU拷贝和NEON拷贝都远快于DMA因为DMA配置DMA描述符、提交任务、等待完成通知这套固定软件开销这平台上大概2~3us在小数据量下占比太高。16KB是转折点DMA开始追平甚至略超NEON。64KB及以上DMA优势明显数据越大优势越大4MB搬运时DMA耗时只有NEON的40%CPU的21%。NEON始终比CPU快约1.9~2倍符合之前说的理论预期。3.3 数据背后的硬件依据这些数字可以解释很关键的几个硬件现象CPU拷贝的瓶颈不在内存带宽而在CPU的指令执行宽度和访存队列深度。对于每条load指令CPU从L1 Cache命中还好一旦触及L2/L3甚至内存Latency高得多。NEON拷贝提速的关键是减少指令数以及通过PRFM指令把数据提前搬到Cache里。DMA的优势在于它不占用CPU的指令发射带宽完全独立地在系统总线上搬运数据。但它的延迟高需要CPU配置过程而且要等到整个传输完成或达到设定的阈值条件才能拿到结果。有个很重要的运营经验DMA不是用来“比快”的而是用来“解放CPU”的。即便在某些场景下DMA总耗时略高但因为CPU可以在这段时间去做别的任务系统整体吞吐量反而更高。注意DMA进行内存到内存的拷贝需要在一致性处理上特别小心。如果源缓冲区是CPU刚写入的在启动DMA前需要做Cache Clean如果是设备写内存再拷走读之前往往需要Cache Invalidate。这部分处理不当会出现数据不一致的诡异bug。4. 选型决策什么场景该用哪种4.1 按数据量划分的选型框架这是我在项目里反复验证过的一套判断逻辑你按这个顺序问自己就行第一问数据是从外设进出的吗是直接考虑DMA这是它的主场。比如串口收数据、SPI读写Flash、网卡收发包、屏显更新优先用DMA。第二问数据量和实时性要求如何对几十字节的寄存器配置型数据老老实实用CPU或NEONDMA光是配置开销就不划算。持续大流量的数据搬运比如视频帧、大块日志落盘DMA是正解。第三问CPU占用率是不是瓶颈如果项目里的CPU已经很紧张4核占了3.7个核的平均负载那大块数据拷贝就值得用DMA解放CPU如果CPU空闲得很用memcpy反而实现最简单。第四问代码可移植性要求是不是很高NEON和DMA都有平台相关性。纯memcpy哪都能跑但性能就差一些。对需要跨ARM、x86、RISC-V跑的通用库代码通常先上纯C版本的memcpy再把NEON作为编译选项下的优化分支。4.2 典型场景决策表典型场景推荐方案理由串口收几十字节不定长数据DMAIDLE中断串口速率低但DMA能降低CPU中断频率SPI读Flash某扇区4KBNEON或DMA4KB接近分界线两者都可数据量大用DMA视频帧YUV420搬运1080p30fpsDMA每帧约3MBDMA能省出大量CPU音频PCM数据拷贝48000Hz, 16bit, 双声道DMA周期性持续传输CPU不能一直忙于搬运修改一块数据结构里的字段CPU直接访问只有几十字节DMA毫无优势图像旋转、像素格式转换后拷贝NEON涉及计算DMA只能搬不能算NEON可以边算边搬跨平台通用库的默认拷贝CPU memcpy兼容性最好性能下限有保证4.3 DMA内部细节的深入讲解DMA方案一旦确定还有几个技术细节值得展开描述符链Descriptor ChainDMA不是只能一次性搬固定大小的数据块。很多DMA控制器支持把多个传输描述符串成一个链表驱动把描述符配置好后DMA引擎会自动依次执行完成最后一个描述符才发中断。这种机制非常适合流式数据比如网络驱动里把多个socket缓冲区的离散数据聚合成一个完整的网络报文。环形缓冲Ring Buffer收发方向各维护一个环形描述符队列驱动和DMA引擎通过读写指针协作。网卡驱动几乎都是这套设计。用环形缓冲可以持续搬运数据不需要反复分配/释放缓冲区这在高吞吐场景里特别重要。DMA连续请求Continuous DMA Requests有的DMA控制器支持连续传输模式只要描述符就绪就自动启动下一次传输不需要每次等CPU重新配置。这能进一步降低CPU的介入频率在高速外设场景比如MIPI摄像头进数据几乎是必须的。Cache一致性策略DMA缓冲区在设备树或驱动里通常标记为dma_alloc_coherent一致性映射这种缓冲区硬件会保证Cache一致性但分配开销大。另一类是dma_map_single按需映射每次使用前做Sync操作Clean/Invalidate比较适合低频大块传输。选哪种得看你的传输频率。高频小包用一致性缓冲区反而性能更好因为省掉了反复Sync的调用开销。4.4 NEON实现细节再透一层我自己写NEON拷贝的经验是不要自己造轮子但要有能力造轮子。一般你需要的场景就下面几种需要把memcpy换成NEON加速版本ARM的libc里memcpy已经针对NEON做了优化直接链接系统库就有效能你在用户态调用memcpy其实已经能吃到NEON红利。需要处理像素格式转换后拷贝这时候用NEON做“边转换边写回”最合适避免先用普通拷贝搬一次再做一轮转换。比如把RGBA转成NV12用NEON搬运的同时做颜色空间计算效率提升非常可观。需要写Neon intrinsics代码ARM提供了arm_neon.h头文件时记得几个关键点#include arm_neon.h void neon_copy_64bytes_aligned(uint8_t *dst, const uint8_t *src) { uint8x16_t a0 vld1q_u8(src); uint8x16_t a1 vld1q_u8(src 16); uint8x16_t a2 vld1q_u8(src 32); uint8x16_t a3 vld1q_u8(src 48); vst1q_u8(dst, a0); vst1q_u8(dst 16, a1); vst1q_u8(dst 32, a2); vst1q_u8(dst 48, a3); }在写大型NEON拷贝函数时我一般会保证对齐先用普通字节拷贝处理头尾未对齐部分让src和dst中对齐的大块交给NEON指令处理。多组并行加载一次把4个NEON寄存器加载完再统一store让访存和写回流水化实测比“load-store-load-store”的次序性能高不少。加PRFM每处理完一块预取下一块数据到Cache隐藏内存延迟。ARM的libc就是这么干的。注意Cache行大小ARM64常见的Cache Line是64字节NEON一次可以操作16字节所以用4个NEON寄存器刚好覆盖一个Cache Line这是对齐优化的重要基础。4.5 几种流式拷贝的综合经验总结在项目里做选型经常不是“三选一”而是“组合拳”数据从外设进来先用DMA搬到一段DMA缓冲区送到应用层之前需要做格式转换时转转换这段用NEON根据运算结果可能只需要一小部分数据那再按需拷贝这一部分用CPU memcpy就行。三种方式各司其职各自处理自己最擅长的那个环节整体效率和CPU占用都能优化到比较理想的状态。5. 一个完整案例串口DMA接收不定长数据选型框架讲完了用一个人实战案例把整套思路串起来。串口接收在嵌入式开发里极常见而且很多人问“怎么用DMA收不定长数据”原理和代码把它拆透了后面很多外设都适用。5.1 需求与方案的思考场景MCUSTM32F103为例通过串口接收来自上位机的命令命令长度不固定短则几个字节长可达256字节需要解析并响应。传统做法是开串口接收中断每收到一个字节触发一次中断一次丢进环形缓冲。这在波特率不高、数据量小的时候没事但波特率上到921600一字节一中断会让CPU非常忙碌而且还有中断里读写环形缓冲的锁开销。改进方案用DMA把串口数据直接搬运到内存缓冲区配合空闲中断IDLE Line Interrupt检测一帧数据结束一帧到达后CPU一次性处理整帧数据。为什么这么做串口空闲中断在串口总线空闲一个字节时间后触发正好可以当成“数据帧传输结束”的标志。DMA在后台搬运数据CPU不必每个字节都被中断打扰。5.2 具体实现步骤STM32 HAL库为例配置串口和DMA// 开启串口接收DMA static uint8_t dma_rx_buf[512]; HAL_UART_Receive_DMA(huart1, dma_rx_buf, sizeof(dma_rx_buf));开启串口空闲中断__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);在中断回调里判断帧结束void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint32_t remaining __HAL_DMA_GET_COUNTER(hdma_usart1_rx); uint16_t frame_len sizeof(dma_rx_buf) - remaining; process_frame(dma_rx_buf, frame_len); // 重新启动DMA接收 HAL_UART_Receive_DMA(huart1, dma_rx_buf, sizeof(dma_rx_buf)); } HAL_UART_IRQHandler(huart1); }加点DMA双缓冲增强如果帧率很高可以在处理一帧数据的同时让DMA继续接收下一帧避免在处理期间丢数据。用HAL_UART_Ex_ReceiveToIdle_DMA一次传两个缓冲区DMA会在两个Buffer之间交替填充后台处理刚才填完的那一块典型乒乓缓冲思路。注意STM32F103的DMA和较新的系列F4/H7在DMA控制器设计上有差别F103的DMA不支持多数据流和多缓冲区做双缓冲要手动切换目标地址。HAL库有对应接口新系列会舒服很多。如果是自学/项目选型实在在意串口吞吐的话H7或G4系列是更好的选择。5.3 我踩过的坑串口DMA接收不定长数据我第一版出过不少问题最典型的几个丢帧处理一帧数据耗时太长处理完再去启动新的DMA接收中间这段时间就漏了。解决方法是提高帧处理效率或者用双缓冲/环形缓冲。数据错位DMA接收缓冲区和处理线程之间没有做好同步一边在写一边在读。串口接收中断和业务处理如果不在同一个上下文建议还是拿一个互斥锁或者用无锁队列机制保护尤其在RTOS环境里。帧尾判断误判空闲中断在一帧结束后的确会触发但如果上位机发送时字节之间有间隔可能会被误判成多帧。这种情况可以在协议层加帧头帧尾校验或者用定时器做超时判断。这类问题在“py32f003使用串口DMA方式接收通讯数据使用接收空闲中断判断接收线束”这类热搜里频繁出现说明做的时候大家遇到的问题高度一致提前避坑真的很重要。6. 各平台的DMA差异与移植要点6.1 ARM SoC与STM32的DMA差异标题里搜到的“dma”、“dma测速软件”、“zynq dma”这些热词反映了不同平台对DMA的实现差异巨大STM32的DMA控制器固定通道、固定外设请求映射配置简单传输模式支持单次和循环。但灵活性低复杂多点传输要软件介入。ZynqXilinx FPGA SoC的DMA常用AXI DMA IP支持MM2Smemory to stream和S2MMstream to memory和PL逻辑配合。驱动在Linux下用Xilinx官方DMA驱动承载视频流等大数据搬运很常见。通用Linux平台如i.MX、RK、MTK的DMA引擎内核的dmaengine框架统一抽象了各家DMA控制器驱动里只要用dma_request_channel申请通道、dmaengine_prep_dma_memcpy准备传输、dmaengine_submit提交、dma_async_issue_pending启动、dma_wait_for_async_tx等完成就能完成一次内存到内存的DMA传输。这套API在大多数BSP里开箱就能用。6.2 Linux dmaengine框架实用模板贴一个Linux内核态用dmaengine做memcpy的模板方便有需要的直接改struct dma_chan *chan; struct dma_async_tx_descriptor *tx; dma_cookie_t cookie; dma_addr_t src_dma, dst_dma; unsigned long flags DMA_CTRL_ACK | DMA_PREP_INTERRUPT; chan dma_request_chan(dev, tx); // 或者用 dma_request_slave_channel src_dma dma_map_single(dev, src_ptr, size, DMA_TO_DEVICE); dst_dma dma_map_single(dev, dst_ptr, size, DMA_FROM_DEVICE); tx dmaengine_prep_dma_memcpy(chan, dst_dma, src_dma, size, flags); tx-callback dma_done_callback; tx-callback_param done_flag; cookie dmaengine_submit(tx); dma_async_issue_pending(chan); // 等待完成可用wait_for_completion或者busy poll dma_wait_for_async_tx(cookie); dma_unmap_single(dev, src_dma, size, DMA_TO_DEVICE); dma_unmap_single(dev, dst_dma, size, DMA_FROM_DEVICE); dma_release_channel(chan);这一套流程在公司里面做音频、视频、信号采集的驱动时经常会出现掌握它以后跨平台适配DMA会轻松很多。6.3 关于DMA continuous requests的理解再看一下热词里的“dma continuous requests”。这个说法在不同的上下文里含义略有区别在STM32等MCU的DMA里通常指“循环模式Circular Mode”传输结束后自动回到起始地址持续接收数据到缓冲区环形队列。在部分DMA控制器里指DMA能够连续处理配置好的多个不同描述符不需要CPU在描述符间介入。在PCIE设备驱动领域还可能有Continuous DMA Request相关的DMA重映射或中断合并策略。不管哪种核心目标都是让DMA减少对CPU的依赖做到“设置完就自主运行”。一旦你理解了连续请求的思想再看网卡、声卡这类驱动的实现就会通透很多。比如声卡驱动里DMA持续写入环形缓冲中断只在半满或者全满等几个关键点触发CPU不用频繁介入。7. 常见问题与排错指南7.1 为什么要用DMA反而更慢了这是做DMA选型时最常遇到的挫败。原因多半是数据量太小配置开销占比高前面实测数据里小于16KB场景DMA的固定开销摊不掉总耗时可能反而更高。没有做Cache一致性处理数据源在Cache里还没写回DMA从内存搬的是旧数据CPU对比结果发现数据不对以为是性能问题。频繁中断导致上下文切换开销高如果DMA每次搬几个字节就中断一次CPU在中断进入/退出上的开销可能比直接拷贝数据还贵。排查思路先用perf或者自己的时间戳把DMA整个流程拆开配置耗时/等待耗时/中断耗时/同步耗时看看瓶颈在哪个环节再针对性优化。7.2 DMA实际搬完没有等了多久DMA传完的通知机制有两种中断和轮询。中断适合数据量中等、到达频率不定、CPU有别的任务可做的场景。轮询或者忙等适合数据量很大、延迟要求低的场景像一些高带宽存储测试就用忙等DMA完成标志。选型时还要考虑一个现实问题中断的响应延迟。如果你要求“数据一到立刻处理”中断优先级要设高一点但在高负载系统里中断上下文本身也可能被更高优先级抢占。7.3 为什么同一个平台NEON的加速效果不太明显分几种情况排查编译器版本和优化开关不对要确认编译时加了-O2或-O3且目标架构开了比如-mcpucortex-a72否则NEON内联的代码可能被编译器忽略或生成了普通标量指令。数据没对齐NEON在不对齐内存上也能工作但性能会打折。热点不在拷贝上如果瓶颈在内存延迟而非带宽NEON再优化也就那点效果。内存本身是瓶颈如果数据已经放到Cache里NEON和memcpy的差距确实很小只有从内存拉数据的场景才能体现带宽优势。7.4 需要边搬边算选什么方案这是NEON大显神威的主场。典型应用图像格式转换、像素灰度化、Alpha混合、音频格式转换等。DMA只能搬不能算你用DMA把原始数据搬到目标地址后还得再用CPU/NEON做一遍转换等于搬运和计算两次访问内存。用NEON一步到位“加载数据、立即计算、写回结果”既节省了内存带宽又减少了CPU指令数。举个简单的例子把ARGB8888转RGB888每次去掉一个Alpha字节。用NEON可以加载16个像素用vuzpq_u8或者按偏移量重排数据最后一次性写回压缩后的数据效果非常显著。7.5 项目经验补充工具与性能验证热词里出现了“cpu与gpu”、“cpu cache”、“spec cpu 2006下载”这些检索词说明读者还关心性能验证和对比。做数据搬运选型验证时几个实用工具安利给大家perfLinux自带性能分析神器。perf stat ./test可以快速看到CPU周期、指令数、Cache Miss用来判断瓶颈是带宽还是指令发射。streambench / STREAM经典的内存带宽测试工具可以测出平台的实际内存带宽上限对照自己的拷贝数据就能知道有没有到达硬件理论上限。寄存器和DMA状态调试MCU上调试DMA时直接看DMA状态寄存器确认有没有配置错误、传输正常与否很多诡异问题都是寄存器里某个标志位没有被正确清零。8. 最终建议最适合你的方案组合最后说点私货。我在实际项目里很少会只选一种拷贝方式。做法通常是这样局部操作处理少量数据结构、配置信息直接用CPU的memcpy不要给自己找麻烦。代码可读性最重要性能没有瓶颈就不优化。中等数据量、涉及简单变换格式化、颜色空间、音频重采样前处理优先用NEON实现难度可控而且可以边搬边算省一次内存访问。这在实时视频/音频处理链路里非常常见。持续的大流量数据搬运网络包收发、存储读写、视频流管线必须上DMA而且一定配合描述符链/环形缓冲/多缓冲机制把CPU从搬运工的角色里彻底解放出来。我的流程是先用CPU memcpy把功能跑通保证逻辑正确再用perf看热点确认瓶颈确实在拷贝上数据量大用DMA有计算成分用NEON综合验证后提交代码。按这个思路走既不会在早期优化里浪费时间又能在真正需要优化的时候一步到位。如果只能留一个建议不要把“最快的拷贝方式”当作唯一目标把“系统整体吞吐和CPU占用最优”当作目标。数据搬运只是系统里的一个环节好的工程师会站在系统视角做权衡。这也是DMA、NEON、CPU拷贝三者并行协作的最本质原因。希望这篇文章能帮你在下一次选型时少踩坑多快好省地把数据管好。
返回列表