실전 사례 — 카메라 1080p 60fps가 30fps로 떨어지는 이유
#한 줄 요약
“CPU가 한가한데 throughput이 모자란다면 bus와 DMA를 의심해야 합니다.”
#증상 — 보고된 문제
카메라 영상을 캡처해 디스크에 저장하는 프로토타입에서 frame drop이 보고됐습니다.
HW: Cortex-A53 quad-core, 1.2 GHz, MIPI CSI-2 input요구: 1080p (1920×1080) YUV422, 60 fps → 1920 × 1080 × 2 × 60 = 248 MB/s
증상: 안정적으로 30 fps만 캡처 60 fps 설정 시 50% frame drop CPU idle 70% 이상으로 여유 많음CPU가 한가한데 throughput이 모자라는 전형적인 비-CPU bound 문제였습니다. 처음에는 카메라 센서나 ISP 자체의 한계를 의심했지만, 데이터시트상 센서는 60 fps를 지원했습니다.
#가설 1 — CPU 처리 부하
먼저 CPU 부하를 확인했습니다.
top -H -p $(pidof capture_app)
# PID USER %CPU COMMAND# 1234 root 28.0 capture_app# 1235 root 15.0 cap_dma_thread# 1236 root 8.0 cap_format# 1237 root 4.0 cap_writer캡처 데몬의 모든 thread를 합쳐도 60% 미만. 코어별로 봐도 가장 바쁜 코어가 40% 정도였습니다.
perf stat으로 IPC도 확인했습니다.
perf stat -p $(pidof capture_app) sleep 10
# 1.61 insn per cycle# 0.8% branch-miss# 4.2% cache-missIPC 1.6은 정상 범위였고 cache miss나 branch miss도 문제 수준이 아니었습니다. CPU는 일이 없어서 놀고 있었지 막혀 있는 것이 아니었습니다.
가설 1 기각: CPU 처리는 병목이 아닙니다.
#가설 2 — DMA 성능 부족
CPU가 한가하다면 다음 의심은 데이터 전송 경로입니다. MIPI CSI-2 receiver → DMA → DDR 경로 어딘가에서 throughput이 부족해 보였습니다.
먼저 DMA 컨트롤러의 통계 레지스터를 읽었습니다.
/* 디버그 코드 */printf("DMA xfer count: %u\n", read_reg(DMA_XFER_CNT));printf("DMA error: %u\n", read_reg(DMA_ERR_STATUS));printf("DMA active: %u%%\n", read_reg(DMA_BUSY_PCT));
# DMA xfer count: 124000000# DMA error: 0# DMA active: 98%DMA channel이 98% busy. 거의 항상 일하고 있다는 뜻이지만 이상하게도 throughput은 124 MB/s에 머물고 있었습니다. 이론 bandwidth(DDR3-1600 single-channel 6.4 GB/s)의 2% 수준이었습니다.
AXI bus 효율이 의심됐습니다. 로직 분석기(Saleae 대안으로 SoC가 제공하는 AXI performance monitor)를 붙여 bus transaction을 측정했습니다.
AXI master ports utilization: cam_dma: 98% cpu: 12% gpu: 3% total: ~50% (overlapping)
cam_dma transaction profile: avg burst length: 16 beats beat width: 8 byte → per transaction: 128 byte inter-transaction overhead: ~20 cycles effective efficiency: 50%DMA가 매 transaction마다 128 byte만 전송하고 20 cycle을 idle로 보내고 있었습니다. AXI bus 효율 50%로 throughput의 절반을 overhead로 까먹는 구조였습니다.
가설 2 확정: DMA burst size가 너무 작습니다.
#원인 — Burst Size와 Descriptor 모델
DMA 컨트롤러 드라이버를 확인하니 burst size가 default 16 beat로 설정되어 있었습니다. 데이터시트는 최대 256 beat까지 지원했습니다.
Burst size 16 beat × 8 byte = 128 byte/transactionOverhead 20 cycle @ 1 GHz = 20 ns/transactionUseful transfer: 128 byte / (128/6.4 GB/s + 20 ns) = 128 byte / (20 ns + 20 ns) = 3.2 GB/s × 효율 → 단일 transaction 효율 50%
Burst size 256 beat × 8 byte = 2048 byte/transactionUseful transfer: 2048 byte / (320 ns + 20 ns) = ~94% 효율이론적으로 burst size를 16배 키우면 효율이 50% → 94%로 올라갑니다.
추가로 캡처 드라이버가 single descriptor 방식으로 동작하고 있었습니다. 한 frame을 받으면 CPU가 IRQ에서 다음 descriptor를 setup하고 DMA를 재시동하는 구조였습니다. Setup latency 동안 DMA가 idle 상태였습니다.
60 fps 기준 frame interval 16.6 ms에서 200 us / 16.6 ms = 1.2% 손실처럼 작아 보이지만, 실제로는 frame end-of-line 사이에도 동일 패턴이 발생합니다.
#해결 — Burst Size, Scatter-Gather, Double Buffer
세 가지 변경을 동시에 적용했습니다.
Step 1: Burst size 16 → 256 beat
/* Device tree */&cam_dma { snps,axi-config = <&axi_cfg>;};
&axi_cfg { snps,max-burst-len = <256>; snps,wr-osr-lmt = <15>; /* outstanding write */ snps,rd-osr-lmt = <15>; /* outstanding read */};또는 드라이버 코드에서 직접 설정합니다.
dma_slave_config cfg = { .direction = DMA_DEV_TO_MEM, .src_maxburst = 256, .dst_maxburst = 256,};dmaengine_slave_config(chan, &cfg);DDR3 controller가 256 beat burst를 한 번에 처리하면 row open/close overhead가 분산되어 efficiency가 크게 올라갑니다.
Step 2: Scatter-gather descriptor chain
Single descriptor 대신 ring of descriptors를 미리 만들어 둡니다. 한 frame이 끝나면 hardware가 자동으로 다음 descriptor로 넘어가 CPU 개입이 필요 없습니다.
#define N_DESC 8
struct cam_descriptor desc[N_DESC];dma_addr_t buf[N_DESC];
for (int i = 0; i < N_DESC; i++) { buf[i] = dma_alloc_coherent(dev, FRAME_SIZE, &dma_handle, GFP_KERNEL); desc[i].buf_addr = dma_handle; desc[i].buf_len = FRAME_SIZE; desc[i].next = &desc[(i + 1) % N_DESC]; /* ring */}
dma_engine_start_chain(chan, &desc[0]);이제 CPU는 frame ready 통지만 받고 buffer를 user space로 전달합니다. DMA setup overhead가 사라집니다.
Step 3: Double buffer로 latency hiding
DMA가 다음 frame을 받는 동안 user thread가 이전 frame을 처리합니다. Producer-consumer ring으로 구현합니다.
ring_buffer_t *rb;
/* IRQ handler */void on_frame_done(int idx){ ring_buffer_publish(rb, idx);}
/* Consumer thread */void *processor(void *arg){ while (1) { int idx = ring_buffer_consume(rb); process_frame(buf[idx]); ring_buffer_release(rb, idx); }}DMA와 CPU가 서로 다른 buffer를 동시에 다루므로 frame interval 전체를 활용할 수 있습니다.
#검증 — Before / After
각 변경의 효과를 단계적으로 측정했습니다.
| 단계 | DMA throughput | AXI eff. | 캡처 fps | Frame drop |
|---|---|---|---|---|
| Original | 124 MB/s | 50% | 30 | 50% |
| Burst 256 | 230 MB/s | 92% | 56 | 7% |
| + SG chain | 245 MB/s | 95% | 59 | 2% |
| + Double buffer | 248 MB/s | 96% | 60 | 0.01% |
목표 248 MB/s에 안정적으로 도달했고 1080p 60fps frame drop이 사실상 사라졌습니다. AXI 효율도 50% → 96%로 회복했습니다.
CPU 사용률은 변화 없이 60% 정도를 유지했습니다. DMA 변경이 CPU에는 영향을 주지 않고 transport 효율만 개선한 결과였습니다.
#교훈
이번 사례에서 얻은 교훈을 정리합니다.
- CPU가 한가한데 throughput 부족 = bus 또는 I/O 의심.
top에서 idle이 보이는데 throughput이 안 나오면 CPU는 범인이 아닙니다. DMA, bus, peripheral controller, 또는 memory subsystem 어딘가에서 막혀 있습니다. - DMA burst size는 첫 점검 대상. 많은 driver의 default가 보수적인 값(16 beat 등)입니다. Hardware가 지원하는 최대값에 가깝게 올리면 큰 효과를 봅니다. DDR controller의 row open/close cost가 burst length로 분산되기 때문입니다.
- AXI performance monitor를 활용. SoC가 제공하는 bus performance counter는 거의 사용되지 않지만, throughput 문제 진단에서 결정적인 정보를 줍니다. Datasheet의 PMU 챕터를 꼭 읽어 둡니다.
- Single descriptor는 안티패턴. 모든 frame/transaction마다 CPU 개입이 필요한 구조는 latency hiding이 불가능합니다. Descriptor chain 또는 ring을 미리 만들어 hardware가 자율적으로 동작하게 합니다.
- Double buffer로 idle time 제거. Producer와 consumer가 다른 buffer를 동시에 다루는 패턴은 DMA뿐 아니라 GPU, NPU, codec 등 모든 throughput-critical 경로에서 표준입니다.
- 측정 없이 가정하지 말 것. “DMA가 충분히 빠르겠지”, “burst size는 적당하겠지” 같은 가정이 50% 효율을 그대로 두는 원인이었습니다. 한 번 측정하면 즉시 보입니다.
가장 큰 교훈은 시스템 성능 진단을 CPU 안에만 가두지 않는 것입니다. 임베디드 시스템은 CPU, memory, bus, peripheral이 한 묶음으로 동작하며, 어느 한 곳의 효율이 떨어지면 전체 throughput이 그 한계에 묶입니다. CPU만 측정하는 도구로는 절반의 그림밖에 못 봅니다.
이 사례로 6장 case study series를 마칩니다. 다음은 7장 advanced topics로 PMU customization, ftrace plugin, kernel module level profiling 같은 주제로 들어갑니다.
#관련 항목
Embedded Performance Engineering · 54 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 처리량 회복
관련 글
DMA 성능 최적화 — Burst·Scatter-Gather·Chain·Cache 일관성
Burst size 최적화. Scatter-gather, chain. Cache clean/invalidate, double buffer.
실전 사례 — CXL.mem 추가로 LLM inference KV cache 처리량 회복
70B 모델 KV cache가 HBM 한계를 넘어 throughput이 무너졌을 때, CXL.mem 256 GB pool 추가로 회복한 실전 케이스.
실전 사례 — 8-core가 4-core를 넘으면 throughput이 떨어지는 이유
8-core 서버에서 thread를 늘릴수록 throughput이 오히려 감소. 단일 global mutex가 cache invalidation 폭주를 일으킨 사례.