C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
#한 줄 요약
“측정 없이 최적화 없습니다.”
size,nm,objdump,bloaty네 도구로 어디서 크기가 오는지 추적합니다.
#어떤 문제를 푸는가
빌드를 끝내고 ELF가 예상보다 큰 경우가 있습니다. Flash 용량을 초과하거나 PR이 갑자기 10KB 늘기도 합니다. 어디서 왔는지 모르면 해결할 수 없습니다.
bloat 추적은 추정이 아닌 측정의 영역입니다. 임베디드 표준 도구가 정확히 어느 함수가, 어느 라이브러리가, 어느 인스턴스가 자리를 먹는지 알려줍니다.
이 글은 네 도구의 사용 패턴과 bloat 추적 워크플로를 정리합니다.
#ELF 파일의 구조
크기 분석을 하려면 ELF 섹션을 알아야 합니다.
| 섹션 | 의미 | 위치 (Flash/RAM) |
|---|---|---|
.text | 실행 코드 | Flash |
.rodata | 읽기 전용 데이터 (const, string literal) | Flash |
.data | 초기값 있는 mutable 데이터 | RAM (init from Flash) |
.bss | 0으로 초기화되는 데이터 | RAM |
.init_array | static 생성자 포인터 | Flash |
.fini_array | static 소멸자 포인터 | Flash |
.debug_* | 디버그 정보 | (ELF에만, Flash X) |
.comment, .note | 메타 | (보통 strip) |
Flash 사용량 = .text + .rodata + .data + .init_array.
RAM 사용량 = .data + .bss + heap + stack.
#도구 1 — size
가장 단순한 도구입니다. 전체 섹션 크기의 합계를 보여 줍니다.
arm-none-eabi-size firmware.elf
text data bss dec hex filename 18432 1024 8192 27648 6c00 firmware.elftext: 코드 + read-only 데이터data: 초기값 있는 RAM 데이터bss: 0 초기화 RAM 데이터dec: 위 셋의 합계 (decimal)
첫 점검에 유용합니다. 자주 보는 사용은 다음과 같습니다.
# Berkeley 형식 (위와 같음)arm-none-eabi-size firmware.elf
# SystemV 형식 (섹션별 자세히)arm-none-eabi-size -A firmware.elfarm-none-eabi-size -A -d firmware.elf # decimal
# 여러 파일 비교arm-none-eabi-size *.oCI에 추가해 두면 PR마다 크기 변화를 추적할 수 있습니다. 임계치를 넘으면 fail로 처리합니다.
# CI script 예시size firmware.elf | awk 'NR==2 {if ($4 > 65536) exit 1}'#도구 2 — nm
각 심볼(함수, 변수)의 크기와 위치를 보여 줍니다.
arm-none-eabi-nm --size-sort --print-size firmware.elf | tail -20
00000034 t reset_handler00000080 T main000000a8 t setup_clock00000118 T HAL_GPIO_Init00000234 T HAL_UART_Init00000820 r .rodata.constprop_table000018f0 T __libc_init_array각 줄의 형식은 <주소> <크기> <type> <심볼>입니다.
- 대문자 type(T, R, D)은 global을 의미합니다
- 소문자(t, r, d)는 local을 의미합니다
- T/t는 code(.text)입니다
- R/r은 read-only data(.rodata)입니다
- D/d는 data입니다
- B/b는 bss입니다
- U는 undefined(외부 심볼)입니다
자주 쓰는 옵션은 다음과 같습니다.
# 크기 내림차순nm --size-sort -S firmware.elf | sort -k 2 -r | head -30
# C++ 데맹글링nm --demangle firmware.elf
# 특정 섹션만nm firmware.elf | grep " T " # .text 함수만C++ 디맹글링이 결정적입니다. 다음과 같이 원래 이름이 보입니다.
# nm 그대로00000234 T _ZN6Logger7log_intEPKci
# nm --demangle00000234 T Logger::log_int(char const*, int)#도구 3 — objdump
가장 강력한 도구입니다. 디스어셈블, 섹션 헤더, 심볼 테이블을 모두 다룰 수 있습니다.
# 전체 디스어셈블 (큼)arm-none-eabi-objdump -d firmware.elf > disasm.txt
# 한 함수만arm-none-eabi-objdump -d firmware.elf --disassemble=main
# C++ 디맹글arm-none-eabi-objdump -d -C firmware.elf
# 소스 라인 mapping (with -g)arm-none-eabi-objdump -dS firmware.elf
# 섹션 헤더arm-none-eabi-objdump -h firmware.elf자주 쓰는 패턴은 최적화 결과를 확인하는 용도입니다.
# 어셈블리에서 함수의 정확한 크기와 명령arm-none-eabi-objdump -d -C firmware.elf --disassemble=Logger::log_int출력:
00000234 <Logger::log_int(char const*, int)>: 234: b510 push {r4, lr} 236: 4604 mov r4, r0 238: f7ff fffe bl 400 <printf> 23c: bd10 pop {r4, pc}함수가 8바이트의 ARM Thumb 명령으로 구성됩니다. 코드 리뷰에서 “이 추상화의 비용”을 정확히 보여줍니다.
#도구 4 — bloaty
Google이 만든 현대적 크기 분석기입니다. 사용자 친화적입니다.
# 설치 (macOS)brew install bloaty
# 기본 — 섹션별 크기bloaty firmware.elf
# 심볼별 (큰 순서)bloaty -d symbols firmware.elf | head -30
# 컴파일 유닛별bloaty -d compileunits firmware.elf | head -30
# 두 차원 동시bloaty -d sections,symbols firmware.elf | head -30
# C++ 데맹글bloaty -d symbols --demangle=full firmware.elf가장 강력한 사용은 두 빌드 비교입니다.
# 어느 함수가 자랐는가?bloaty -d symbols --demangle=full firmware_new.elf -- firmware_old.elf출력:
FILE SIZE VM SIZE -------------- -------------- +1.8% +312 +1.8% +312 [Diff] Logger::log_full(...) +0.7% +120 +0.7% +120 [Diff] vtable for ConcreteDevice -0.2% -32 -0.2% -32 [Diff] mainPR이 크기를 늘렸을 때 어느 함수 때문인지 즉시 식별할 수 있습니다. CI에 통합할 만한 가치가 큽니다.
#워크플로 1 — 어디서 크기가 오는가
새 프로젝트에서 처음 ELF가 큰 경우의 추적 절차입니다.
# 1단계: 전체 크기 확인arm-none-eabi-size firmware.elf
# 2단계: 큰 함수 식별arm-none-eabi-nm --size-sort --print-size --demangle firmware.elf | tail -30
# 3단계: 큰 함수의 어셈블리 확인arm-none-eabi-objdump -d -C firmware.elf --disassemble='<큰 함수 이름>'
# 4단계: 라이브러리 의존성 확인arm-none-eabi-nm --undefined-only firmware.elf자주 보는 큰 함수 후보는 다음과 같습니다.
__cxa_throw,__cxa_begin_catch: 예외 관련입니다.-fno-exceptions누락 가능성이 있습니다_dtoa_r,_printf_float: float printf입니다. 정수 printf로 대체합니다vfprintf: printf 전체입니다. 단순 출력 함수로 대체합니다__divdi3,__moddi3: 64-bit divmod입니다. 알고리즘을 재검토합니다operator new,malloc: 동적 할당입니다. 정적 할당을 우선합니다std::__throw_*: STL 예외입니다.-fno-exceptions가 켜지지 않은 상태입니다
#워크플로 2 — PR 크기 변화 추적
# CI에서git checkout mainmake firmware && cp firmware.elf firmware_main.elf
git checkout pr-branchmake firmware && cp firmware.elf firmware_pr.elf
bloaty -d symbols --demangle=full firmware_pr.elf -- firmware_main.elf > size-diff.txtsize-diff.txt를 PR 코멘트에 자동으로 첨부합니다. 예상치 않은 크기 증가를 리뷰 단계에서 발견할 수 있습니다.
#워크플로 3 — 템플릿 bloat 추적
C++ 템플릿은 각 type 인스턴스마다 코드를 생성합니다. 무심하게 쓰면 코드가 중복됩니다.
template<typename T>void process(T value) { // 100 lines}
process<int>(1);process<long>(2);process<int8_t>(3);// → 100 lines * 3 = 300 lines (각 type별)확인 방법은 다음과 같습니다.
nm --size-sort --print-size --demangle firmware.elf | grep "process<"
00000064 T void process<int>(int)00000064 T void process<long>(long)00000064 T void process<signed char>(signed char)세 인스턴스가 보입니다. 대안은 다음과 같습니다.
- 공통 부분을 type-erased 함수로 추출합니다
- concept이나
if constexpr로 분기를 통합합니다 - non-template helper와 thin template wrapper를 결합합니다
자세한 내용은 Part 2-07: Templates 비용 분석에서 다룹니다.
#워크플로 4 — vtable 크기 추적
각 virtual 클래스가 vtable을 만듭니다.
nm --size-sort --print-size --demangle firmware.elf | grep "vtable for"
00000020 V vtable for Logger00000040 V vtable for ConcreteDevice00000018 V vtable for INotifier각 vtable은 virtual 함수 수 × 4바이트입니다. 50개 클래스에 평균 5개 virtual이면 1KB가 됩니다. 극소형 MCU에서는 무시할 수 없습니다.
대안은 CRTP, std::variant + std::visit입니다. 자세한 내용은 Part 2-08에서 다룹니다.
#워크플로 5 — Flash vs RAM 균형
arm-none-eabi-size -A firmware.elf
section size addr.isr_vector 460 8000000.text 18432 80001cc.rodata 4096 80043cc.data 1024 20000000.bss 8192 20000400.heap 4096 20002400.stack 8192 20003400확인 항목은 다음과 같습니다.
- Flash 사용 =
.isr_vector + .text + .rodata + .data= 약 24 KB - RAM 사용 =
.data + .bss + .heap + .stack= 약 21.5 KB - 보드 사양(예: STM32F407 = 1MB Flash, 192 KB RAM)과 비교하면 여유가 큽니다
RAM이 부족하면 다음을 검토합니다.
.bss를 분석해 큰 정적 buffer를 줄입니다static객체는 stack이나 동적으로 옮깁니다- 컴파일 시
-fstack-usage로 함수별 stack 사용을 측정합니다
arm-none-eabi-g++ -fstack-usage -c file.cpp# file.su 파일 생성#라이브러리 추적 — 어느 .o 파일이 큰가
# .o 파일별 크기arm-none-eabi-size build/*.o | sort -k4 -n
# 정적 라이브러리 안 .o 파일 별arm-none-eabi-size -t libfoo.a또는 link map을 활용합니다.
# 링크 시 map 파일 생성LDFLAGS += -Wl,-Map=firmware.map
# map 파일 분석grep "\.text" firmware.map | head -30map 파일은 모든 심볼의 link 결정을 보여줍니다. 예상치 않은 함수가 들어오면 그 호출 chain을 추적합니다.
#자주 보는 함정과 안티패턴
#1. 디버그 정보를 포함한 크기로 비교
size는 디버그를 제외하고 보여줍니다. ls -l firmware.elf는 포함한 크기입니다. 혼동에 주의합니다. Flash에 실제로 들어가는 크기는 arm-none-eabi-objcopy -O binary 결과를 봅니다.
#2. strip 후 분석
strip된 ELF는 심볼 정보가 없습니다. nm과 objdump가 무의미해집니다. strip 전 ELF로 분석합니다.
#3. 함수가 인라인으로 사라진 뒤 찾음
nm에 없는 함수는 인라인된 상태입니다. -fno-inline-functions를 임시로 켜서 확인할 수 있습니다(디버깅 목적).
#4. .o 파일 크기 합계는 ELF 크기와 다름
링커가 gc-sections와 LTO로 크기를 줄입니다. .o 합계는 상한일 뿐입니다.
#5. RAM 부족인데 Flash만 분석
.bss가 큽니다. size로 RAM도 함께 확인합니다.
#6. bloaty 없이 nm만 사용
복잡한 프로젝트에서는 비교가 어렵습니다. PR 변화 추적에는 bloaty를 권장합니다.
#측정 — 한 임베디드 프로젝트의 분석
실제 STM32F4 + FreeRTOS + UART 프로젝트의 Flash 사용 분포입니다.
arm-none-eabi-size firmware.elf text data bss dec hex filename 68192 2048 24576 94816 1722c firmware.elf
bloaty -d symbols --demangle=full firmware.elf | head -10 FILE SIZE VM SIZE -------------- -------------- 18.4% 12552 18.4% 12552 HAL driver functions (STM32 HAL) 14.2% 9696 14.2% 9696 FreeRTOS internals 8.1% 5520 8.1% 5520 USB stack 6.5% 4432 6.5% 4432 printf (정수 only) 5.8% 3952 5.8% 3952 .rodata strings 3.2% 2180 3.2% 2180 Application logic (C++) 2.9% 1978 2.9% 1978 libgcc helpers 2.1% 1432 2.1% 1432 Logger (vtable + impl) 1.8% 1228 1.8% 1228 Ring buffer ...HAL과 FreeRTOS가 절반을 차지합니다. application 자체는 3%에 불과합니다. 임베디드에서는 프레임워크 비용이 가장 큽니다.
#정리
- 측정 도구는 네 가지입니다 —
size(총량),nm(심볼),objdump(어셈블리),bloaty(분석/비교). - C++ 심볼은 mangled되므로
--demangle(-C)로 디맹글이 필수입니다. - 큰 함수 후보는 예외, float printf, 64-bit divmod, dynamic alloc, STL throw입니다.
- PR 크기 변화는
bloaty로 두 ELF를 비교하고 CI에 통합합니다. - Flash와 RAM을 따로 추적합니다.
.bss가 자주 잊혀지는 RAM 소비 원인입니다.
#관련 항목
- Part 1-02: 컴파일러 플래그 — 크기 줄이는 플래그
- Part 1-03: 런타임 요구사항 — libstdc++/newlib 크기 기여
- Part 2-07: Templates 비용 분석 — 템플릿 bloat 추적
- Part 4-02: ETL 라이브러리 — heap 없이 STL 대체
#다음 글
Part 1-05: ABI 호환성 — C와 C++가 같이 살 때 name mangling과 ABI가 만드는 함정과 해결책을 다룹니다.
Embedded C++ for Real Systems · 5 of 41
- 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
- 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
- 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
- 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
- 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
- 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
- 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
- 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
- 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
- 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
- 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
- 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
- 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
- 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
- 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
- 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
- 17CRTP 패턴 분석 — vtable 없는 정적 다형성
- 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
- 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
- 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
- 21Custom Allocator 기초 — std::allocator 인터페이스 분석
- 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
- 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
- 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
- 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
- 26std::expected 분석 — C++23 결과 타입과 에러 전파
- 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
- 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
- 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
- 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
- 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
- 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
- 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
- 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
- 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
- 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
- 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
- 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
- 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
- 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
- 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
관련 글
임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교
범용 HAL 구조 — 벤더 종속성 격리, 다중 보드/MCU 지원, 시리즈 마무리.
Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
UART, SPI, I2C — peripheral을 type-safe class로. Blocking, interrupt, DMA 패턴.
GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
GPIO pin = type — 컴파일 타임에 핀 설정 검증, runtime 비용 0.
이 글을 참조하는 글 (7)
- 임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교— Embedded C++ for Real Systems
- Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정— Embedded C++ for Real Systems
- 임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적— Embedded C++ for Real Systems
- 임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석— Embedded C++ for Real Systems
- 임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os— Embedded C++ for Real Systems
- 임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교— Embedded C++ for Real Systems
- 임베디드 코드 크기 최적화 — -Os·LTO·Section Garbage Collection— Modern Embedded Recipes