DMA vs CPU Copy 성능 비교 — Break-even·Setup Overhead 실측
#한 줄 요약
“DMA가 항상 빠른 것은 아닙니다.” 작은 transfer에서는 CPU memcpy가 우세합니다.
#CPU memcpy 성능 (Cortex-M4)
memcpy(dst, src, len);Newlib 또는 컴파일러 inline 버전:
| len | Cycle | Bytes/cycle |
|---|---|---|
| 16 | 12 | 1.3 |
| 64 | 28 | 2.3 |
| 256 | 92 | 2.8 |
| 1024 | 350 | 2.9 |
| 4096 | 1400 | 2.9 |
168 MHz Cortex-M4 기준으로 4 KB copy가 8 µs입니다.
#DMA setup overhead
HAL_DMA_Start(&hdma, src, dst, len);| 단계 | Cycle |
|---|---|
| Register write (CR·NDTR·PAR·MAR) | ~30 |
| Enable transfer | ~5 |
| IRQ handler 진입·종료 | ~50 |
| Wake task (RTOS) | ~100 |
| Total overhead | ~200 cycle |
200 cycle은 약 60 byte의 CPU memcpy에 해당합니다.
#Break-even Point
즉 단순 cycle 비교만으로는 DMA가 빠를 수 없습니다. 하지만 CPU는 transfer 중에 다른 일을 못 하므로 실제 이득은 CPU offload에서 옵니다.
실제 break-even은 CPU가 할 다른 일에 달려 있습니다. CPU가 idle이면 CPU copy가 빠릅니다.
#실 break-even - CPU가 다른 일 할 때
// Path A: CPU copy, 다른 일 못 함memcpy(dst, src, 1024); // 350 cycle CPU 점유do_other_work(); // 그 후 시작
// Path B: DMA + CPU 병행HAL_DMA_Start(...); // 30 cycle setupdo_other_work(); // 병행 가능wait_dma(); // DMA 완료 대기 (또는 IRQ)Path B의 이득은 do_other_work 시간만큼입니다. 작은 transfer는 setup이 곧 끝나서 이득이 적습니다.
Rule of thumb: 64 byte 미만은 CPU, 256 byte 초과는 DMA. 그 사이는 직접 벤치해야 합니다.
#4-byte word memcpy (Cortex-M4 optimal)
void fast_memcpy_word(uint32_t *dst, const uint32_t *src, size_t words) { for (size_t i = 0; i < words; i++) dst[i] = src[i];}Cortex-M4의 ldr/str은 각각 2 cycle로 1.0 byte/cycle 수준입니다.
Loop unroll + LDM/STM:
void fast_memcpy_ldm(uint32_t *dst, const uint32_t *src, size_t words) { while (words >= 8) { /* ldmia / stmia — multiple register */ uint32_t r0 = src[0], r1 = src[1], r2 = src[2], r3 = src[3]; uint32_t r4 = src[4], r5 = src[5], r6 = src[6], r7 = src[7]; dst[0] = r0; dst[1] = r1; dst[2] = r2; dst[3] = r3; dst[4] = r4; dst[5] = r5; dst[6] = r6; dst[7] = r7; src += 8; dst += 8; words -= 8; } while (words--) *dst++ = *src++;}8-word burst를 쓰면 bus 효율이 좋습니다.
#ARM Cortex-A NEON memcpy
#include <arm_neon.h>
void neon_memcpy(uint8_t *dst, const uint8_t *src, size_t n) { while (n >= 64) { vst4q_u8(dst, vld4q_u8(src)); // 64 byte = 4 × 16-byte vector src += 64; dst += 64; n -= 64; } /* tail */ memcpy(dst, src, n);}DDR bandwidth를 saturate시켜서 DMA와 비슷한 속도를 냅니다.
#glibc memcpy - Optimal
glibc memcpy (ARM aarch64)는 다음 기법을 씁니다.
- SIMD vector load/store
- Non-temporal (큰 size일 때)
- Loop unroll 64-byte burst
- Page-aligned check + dispatch
수십 KB 이상이면 DMA와 동등한 속도가 나옵니다. CPU offload 효과만 다릅니다.
#DMA가 유리한 경우
#1. Peripheral ↔ Memory
HAL_UART_Receive_DMA(&huart, rx_buf, 256);/* CPU 자유 — 다른 일 가능 *//* UART byte 도착 시 DMA 자동 transfer */CPU polling은 CPU 100% blocking이지만 DMA는 CPU 0% (가끔 IRQ만)으로 동작합니다.
#2. 연속 / 주기 transfer
/* Camera frame — 매 frame 30 fps × 1080p × 2 byte = 60 MB/s */HAL_DCMI_Start_DMA(&hdcmi, MODE_CONTINUOUS, frame_buf, frame_size);CPU copy로 처리하면 매 frame 33 ms 동안 CPU 100%로 전체 CPU를 점유합니다. DMA는 0%입니다.
#3. Cache pollution 회피
/* 큰 buffer copy — CPU 시 cache evict pressure */memcpy(dst, src, 1MB);/* → hot working set 깨짐 */DMA는 cache를 우회할 수 있고 non-temporal도 지원합니다. 덕분에 CPU가 하는 다른 일의 cache가 보존됩니다.
#CPU가 유리한 경우
#1. Small transfer
memcpy(&header, src, 16); // ← DMA 설정 시간 더 김#2. 한 번만, CPU idle
load_config(buf, 4096); // boot 시 한 번/* CPU 어차피 할 일 없음 */#3. Cortex-M0/M1 (DMA 없거나 1 channel)
/* DMA channel 부족 — UART/SPI 우선 점유 */#측정 - Benchmark
__DSB(); uint32_t t0 = DWT->CYCCNT;memcpy(dst, src, len);__DSB(); uint32_t t1 = DWT->CYCCNT;printf("CPU: %u cycle\n", t1 - t0);
__DSB(); t0 = DWT->CYCCNT;HAL_DMA_Start(...);while (!dma_done) {}__DSB(); t1 = DWT->CYCCNT;printf("DMA: %u cycle\n", t1 - t0);각 size별로 측정해 break-even chart를 만듭니다.
#STM32H743 실측 (170 MHz Cortex-M7 + 1.4 GB/s DDR3)
| Size | CPU memcpy | DMA | DMA + CPU 다른 일 |
|---|---|---|---|
| 32 B | 28 cycle | 220 cycle | 220 - other |
| 256 B | 200 cycle | 350 cycle | 350 - other |
| 4 KB | 2,800 cycle | 3,200 cycle | 3,200 - other |
| 64 KB | 45,000 | 50,000 | 50,000 - other |
CPU offload 효과를 빼면 둘 다 비슷합니다. 다만 CPU가 다른 일을 할 수 있는 상황에서는 DMA가 압승입니다.
#Cortex-A - memcpy 라이브러리
| 라이브러리 | 특징 |
|---|---|
| glibc | NEON + non-temporal + page check |
| musl | 작은 코드, 보통 성능 |
| Bionic (Android) | SoC-specific 최적화 |
| Cortex-Strings (Arm) | per-Cortex 최적 |
-mtune=cortex-a72로 컴파일하면 적절한 memcpy가 자동으로 선택됩니다.
#DMA 손해 보는 함정
⚠️ 매번 setup
for (i = 0; i < 1000; i++) { HAL_DMA_Start(...); // ← 매번 200 cycle overhead wait_dma();}one-shot 큰 transfer나 chain transfer로 묶는 것이 좋습니다.
⚠️ CPU가 polling
HAL_DMA_Start(...);while (DMA->NDTR != 0); // ← CPU 100% busyIRQ나 task sleep으로 바꿔야 합니다.
⚠️ Buffer alignment 비매칭
uint8_t buf[100]; // 보장 align 1HAL_DMA_Start(..., buf, ...); // ← word align 안 됨 → split__attribute__((aligned(32)))로 정렬을 명시해야 합니다.
#자주 하는 실수
⚠️ 작은 copy도 DMA로
HAL_DMA_Start(&hdma, &val, ®, 4); // ← overkillARMv7의 stm은 1 cycle인데 DMA는 200 cycle입니다. register 한 개 쓰기에는 직접 write가 맞습니다.
⚠️ DMA의 cache 영향 무시
큰 transfer DMA에서 non-cacheable을 쓰지 않으면 evict pressure로 working set이 깨집니다.
⚠️ memcpy 대신 직접 loop
for (i = 0; i < N; i++) dst[i] = src[i];-O2에서는 컴파일러가 알아서 memcpy로 변환합니다. 다만 volatile은 변환되지 않으므로 직접 memcpy(dst, src, N)을 쓰는 편이 안전합니다.
⚠️ DMA throttle
DMA가 너무 빠르면 bus saturation이 일어나 다른 master가 starvation에 빠집니다. bandwidth limiter를 설정해야 합니다.
#정리
- DMA setup overhead는 60-200 byte 등가의 CPU 작업에 해당합니다.
- 작은 copy(<64 B)는 CPU, 큰 copy와 peripheral은 DMA가 유리합니다.
- 진짜 이득은 CPU offload이며 CPU가 다른 일을 할 때만 의미가 있습니다.
- Cortex-M4 memcpy는 ldmia/stmia 최적화 기준 약 3 byte/cycle입니다.
- Cortex-A NEON memcpy는 DDR bandwidth를 saturate시킵니다.
- size별로 직접 측정해서 break-even을 확인해야 합니다.
다음 편은 Interrupt Latency를 다룹니다.
#관련 항목
Embedded Performance Engineering · 23 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 효율이었다.
DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
Burst size 최적화. Scatter-gather, chain. Cache clean/invalidate, double buffer.
Speculative Execution 분석 — OoO·Reorder Buffer·Register Renaming
Out-of-order execution. ROB·issue queue·rename. Spectre 측면. Cortex-A 사례.