DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
#한 줄 요약
“DMA는 CPU를 우회한 데이터 이동입니다.” burst size와 alignment, cache가 핵심을 결정합니다.
#DMA Throughput 결정 요소
| 요소 | 영향 |
|---|---|
| Burst size | 클수록 throughput이 올라갑니다 (overhead 분산) |
| Beat size | bus 폭에 맞춥니다. 32-bit bus는 32-bit beat가 적합합니다 |
| Alignment | misaligned면 split이 발생합니다 |
| Outstanding | DMA controller가 동시에 처리하는 transaction 수입니다 |
| Bus contention | 다른 master의 영향을 받습니다 |
#Burst Size 영향
/* 매 transfer 1 byte */HAL_DMA_Init(&hdma, .PeriphDataAlignment = DMA_PDATAALIGN_BYTE);/* → AXI overhead per byte → 매우 느림 */
/* 4-beat burst, 4-byte beat */HAL_DMA_Init(&hdma, .PeriphDataAlignment = DMA_PDATAALIGN_WORD, .BurstSize = DMA_BURST_INC4);/* → 16-byte per transaction → 10x throughput */Burst length × Beat size가 한 AXI transaction의 크기입니다. cache line 크기인 64-byte가 sweet spot입니다.
#STM32H7 MDMA - Master DMA
MDMA_HandleTypeDef hmdma;hmdma.Init.Request = MDMA_REQUEST_SW;hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER;hmdma.Init.Priority = MDMA_PRIORITY_HIGH;hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS_PRESERVE;hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD;hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACKENABLE;hmdma.Init.BufferTransferLength = 128; // burst length
HAL_MDMA_Start(&hmdma, src, dst, len, 1);MDMA는 AXI master여서 DRAM ↔ DRAM copy까지 가능합니다. 일반 DMA1/2는 peripheral ↔ memory만 지원합니다.
#Scatter-Gather
Source: Destination:[fragment 1 @0x1000] [block @0x80000000][fragment 2 @0x4500][fragment 3 @0x9100][fragment 4 @0xC200]DMA descriptor list:
typedef struct { uint32_t src; uint32_t dst; uint32_t len; uint32_t next_descriptor; // null = 종료} dma_desc_t;
dma_desc_t descriptors[4] = { { .src=0x1000, .dst=base+0, .len=256, .next_descriptor=&descriptors[1] }, { .src=0x4500, .dst=base+256, .len=512, .next_descriptor=&descriptors[2] }, { .src=0x9100, .dst=base+768, .len=128, .next_descriptor=&descriptors[3] }, { .src=0xC200, .dst=base+896, .len=256, .next_descriptor=NULL },};CPU 개입이 전혀 없습니다. DMA controller가 전체 chain을 자동으로 처리합니다.
#Chain Transfer (Linked List)
Frame 1 capture: DMA → buf_a complete IRQFrame 2 capture: DMA → buf_b complete IRQ ...Camera·display·audio에서 연속 스트림을 처리할 때 씁니다. CPU는 processing에만 집중합니다.
#Double Buffer
Buf A: filled by DMA | Buf B: processed by CPU swapBuf A: processed | Buf B: filled/* STM32 DMA — Double Buffer mode */HAL_DMAEx_MultiBufferStart(&hdma, src, dst_a, dst_b, len);
void HAL_DMA_M0CpltCallback(DMA_HandleTypeDef *hdma) { /* dst_a 완료 — process_a() */}void HAL_DMA_M1CpltCallback(DMA_HandleTypeDef *hdma) { /* dst_b 완료 — process_b() */}DMA와 CPU가 동시에 동작하면서 throughput이 약 2배로 늘어납니다.
#Cache 일관성 - Cortex-M7
#CPU가 buffer write → DMA가 send
fill_data(tx_buf, 256); // CPU writes (cache only)SCB_CleanDCache_by_Addr(tx_buf, 256); // ← memory에 flushHAL_UART_Transmit_DMA(&huart1, tx_buf, 256);Clean은 cache 내용을 memory로 write-back합니다.
#DMA가 buffer fill → CPU가 read
HAL_UART_Receive_DMA(&huart1, rx_buf, 256);/* DMA complete */SCB_InvalidateDCache_by_Addr(rx_buf, 256); // ← cache 폐기process(rx_buf);Invalidate는 stale cache line을 폐기합니다. 이후 CPU read는 memory에서 fresh하게 읽어옵니다.
#Misalignment 함정
SCB_CleanDCache_by_Addr(buf, 100); // 100 byteCache line이 32 byte라고 가정합니다. buf가 32-byte aligned가 아니거나 len이 line 배수가 아니면 이웃 line까지 영향을 받습니다.
__attribute__((aligned(32))) uint8_t buf[128]; // line alignedSCB_CleanDCache_by_Addr(buf, 128); // 4 line, exact#Non-Cacheable Memory 영역
/* MPU로 non-cacheable region 설정 */MPU_Region_InitTypeDef region = {0};region.BaseAddress = 0x24000000;region.Size = MPU_REGION_SIZE_512KB;region.AccessPermission = MPU_REGION_FULL_ACCESS;region.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;region.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;HAL_MPU_ConfigRegion(®ion);
/* 이 영역 변수는 cache 관리 불필요 */__attribute__((section(".uncached"))) uint8_t dma_buf[4096];DMA 전용 buffer는 non-cacheable로 두면 cache maintenance overhead가 사라집니다.
#DMA Latency
Setup overhead - 매 transfer 시작 비용:
- STM32 DMA: ~5 cycle
- i.MX SDMA: ~50 cycle (FW 실행)
- Linux dmaengine: ~100 cycle + IRQ
작은 transfer (< 256 byte)에서는 DMA setup 비용이 더 큽니다. 이 구간은 CPU memcpy가 빠릅니다.
#CPU vs DMA - 손익 분기점
- memcpy: 1 byte ≈ 0.5 cycle (Cortex-M4)
- DMA setup
50 cycle + transfer cost (1 byte/cycle)
실제로는 DMA가 CPU를 자유롭게 만들어 CPU offload가 진짜 이득입니다.
작은 copy는 CPU가 유리합니다. 큰 copy(>256 byte)는 DMA가 유리합니다. CPU가 다른 일을 할 수 있을 때는 DMA가 무조건 유리합니다.
#Linux dmaengine API
struct dma_chan *chan = dma_request_chan(dev, "rx");struct dma_async_tx_descriptor *tx;
tx = dmaengine_prep_dma_memcpy(chan, dst, src, len, DMA_PREP_INTERRUPT);tx->callback = dma_done_cb;dma_cookie_t cookie = dmaengine_submit(tx);dma_async_issue_pending(chan);
/* Wait */dma_sync_wait(chan, cookie);Linux에서는 peripheral driver가 dmaengine을 사용해 DMA controller를 추상화합니다.
#DMA, IRQ, Polling 비교
| 방식 | CPU 사용 | Latency | Throughput |
|---|---|---|---|
| Polling | 100% | 매우 낮음 | bus 한계 |
| IRQ per byte | 높음 (overhead) | 낮음 | 낮음 |
| IRQ per buffer | 낮음 | buffer 단위 | 보통 |
| DMA + IRQ | 매우 낮음 | buffer 단위 | bus 한계 |
| DMA chain | ~0 | 0 (CPU 안 봐도 됨) | bus 한계 |
#자주 하는 실수
⚠️ DMA buffer를 stack에
void send_data(void) { uint8_t buf[256]; // ← stack fill(buf); HAL_UART_Transmit_DMA(&huart, buf, 256); return; // ← buf out of scope, DMA 진행 중}DMA buffer는 static, heap, 전역 중 하나에 둬야 합니다. Stack 위에 두면 함수 return 시점에 깨집니다.
⚠️ Cache flush 안 함
DMA가 cache stale data를 보거나 CPU가 옛 data를 읽게 됩니다. 매 transfer 전후로 clean/invalidate를 수행해야 합니다.
⚠️ MPU 영역 잘못 설정
region.Size = MPU_REGION_SIZE_4KB;DMA buffer가 region 경계를 넘어가면 일부는 cacheable, 일부는 non-cacheable이 됩니다. 결과는 미정의 동작입니다.
⚠️ Burst size > slave 지원
Peripheral FIFO depth가 4인데 burst length가 16이면 slave가 4-cycle마다 backpressure를 걸어 throughput이 손실됩니다.
#정리
- DMA 성능은 burst size, alignment, outstanding, bus contention으로 결정됩니다.
- Scatter-gather와 chain으로 CPU 개입을 최소화합니다.
- Double buffer로 throughput을 2배로 끌어올립니다.
- Cortex-M7에서는 clean/invalidate가 필수이고 non-cacheable region도 선택지입니다.
- 작은 copy는 CPU memcpy, 큰 copy와 peripheral은 DMA가 유리합니다.
다음 편은 DMA vs CPU 손익 분석을 다룹니다.
#관련 항목
Embedded Performance Engineering · 22 of 57
- 1Embedded Performance Engineering — 임베디드 성능 엔지니어링 시리즈 소개
- 2임베디드 성능 분석 방법론 — Measure → Analyze → Optimize 사이클
- 3성능 지표 정의 — Latency·Throughput·Utilization 분석
- 4성능 측정의 기본 — Wall-Clock·CPU Cycle·Instruction Count
- 5성능 데이터 통계적 분석 — Percentile·Histogram·평균의 함정
- 6실시간 성능 분석 — WCET·Jitter·Deadline Miss 측정
- 7임베디드 벤치마킹 기초 — 재현성·Warmup·노이즈 제거
- 8성능 모델링 — Amdahl·Gustafson·Roofline Model 적용
- 9프로파일링 기법 개요 — Sampling vs Instrumentation·PGO·LTO
- 10CPU 파이프라인 분석 — 5-stage·Cortex-M·Cortex-A 비교
- 11Pipeline Stall 분석 — Data·Structural·Control Hazard·Forwarding
- 12Branch Prediction 분석 — Static·2-bit·BTB·BHT·Mispredict 비용
- 13Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
- 14CPU Cache 기초 — L1·L2·L3·Set Associative·Replacement Policy
- 15Cache Miss 3C Model 분석 — Compulsory·Capacity·Conflict
- 16Cache Line 최적화 — Alignment·Prefetch·False Sharing 처리
- 17메모리 대역폭 분석 — STREAM·Roofline·Bus Saturation 측정
- 18SIMD·NEON 활용 — 128-bit Vector·Auto-Vectorization·SVE/SVE2
- 19PMU·HPM 하드웨어 카운터 분석 — 정밀 성능 진단
- 20임베디드 Bus Architecture — AHB·AXI·CHI 진화와 5-Channel
- 21Bus Contention 진단 — Arbitration·QoS·Starvation 측정
- 22DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
- 23DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
- 24Interrupt Latency 분석 — 진입·종료·Tail-Chaining·Late Arrival
- 25Interrupt Storm 처리 — NAPI·Rate-Limit·Polling 전환
- 26MMIO 접근 성능 — Cache Policy·Write-Combining·Volatile·Barrier
- 27Peripheral Clock 분석 — PLL·Divider·Gating·DVFS
- 28Power vs Performance 트레이드오프 — DVFS·Race-to-Idle·Big.LITTLE
- 29Thermal Throttling 분석 — Junction Temp·Trip Point·냉각
- 30CXL Interconnect 분석 — AI 시대 메모리 대역폭 확장
- 31Concurrency 기초 — Concurrency vs Parallelism·Race·Memory Model
- 32False Sharing 진단 — Cache Line Ping-Pong·Padding·측정
- 33Lock Contention 분석 — Wait·Hold·Convoy·측정 기법
- 34Spinlock 성능 분석 — Spin-Wait vs Context Switch·Ticket·MCS
- 35Mutex 성능 분석 — Futex·Adaptive·Priority Inheritance
- 36Reader-Writer Lock 성능 — Reader/Writer Priority·RCU·Seqlock
- 37Lock-Free 자료구조 성능 — CAS·ABA·Hazard Pointer·Epoch Reclamation
- 38Memory Ordering 분석 — Acquire·Release·Seq-Cst·ARM Relaxed Model
- 39Cache Coherency 프로토콜 — MESI·MOESI·Snoop·Directory
- 40SMP 성능 분석 — Per-Core·Affinity·Load Balance·Scalability
- 41Linux perf 기초 — stat·record·report 활용
- 42Linux perf 고급 — Raw Event·Tracepoint·perf script
- 43ftrace 활용 — function·function_graph·latency tracer
- 44eBPF·bpftrace 동적 트레이싱 — 커널 무수정 관측
- 45Flamegraph 분석 — On-CPU·Off-CPU·Differential
- 46ARM DS·Lauterbach 분석 — Hardware Trace 전문 도구
- 47Bare-metal 프로파일링 — GPIO·DWT·SysTick·ITM 활용
- 48NVIDIA Nsight Systems — GPU·NPU 포함 시스템 분석
- 49모던 프로파일러 비교 — Tracy·Hotspot·uftrace·Coz
- 50연속 프로파일링 — Parca·Pixie·Pyroscope·Tetragon
- 51실전 사례 — ISR Latency 100µs Deadline Miss 추적
- 52실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
- 53실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
- 54실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
- 55CXL.mem 지연·대역폭 실측 — Direct·Switch·Pooled 토폴로지 비교
- 56CXL 성능 프로파일링 도구 — cxl-cli·DAMON·perf-mem 활용
- 57실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
관련 글
실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
Cortex-A 보드의 카메라 캡처가 frame drop. CPU는 한가했고 진짜 범인은 DMA burst size와 AXI bus 효율이었다.
실전 사례 — Matrix Multiply가 예상의 10배 느린 이유
1024×1024 matrix multiply가 이론값의 10배 느렸다. SIMD부터 의심했지만 진짜 범인은 캐시 미스 90%였다.
DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
DMA setup overhead. CPU memcpy 최적화. Break-even size. 실측 데이터.
이 글을 참조하는 글 (8)
- 실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유— Embedded Performance Engineering
- DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측— Embedded Performance Engineering
- Bus Contention 진단 — Arbitration·QoS·Starvation 측정— Embedded Performance Engineering
- PCIe Streaming 분석 — BAR Type·MSI-X·Kernel Bypass— Modern Embedded Recipes
- Command Queue·Submission Queue — NVMe·XDMA 공통 패턴— Modern Embedded Recipes
- Zero-Copy Pipeline — DMA-BUF·sendfile·io_uring·splice— Modern Embedded Recipes
- DMA-Friendly Allocator — dma_alloc_coherent·IOMMU·Pool— Modern Embedded Recipes
- UIO·VFIO 분석 — User-Space Driver와 IOMMU 격리— Modern Embedded Recipes