GN⁺: {fmt} 축소: 이진 크기 14k로 줄이고 C++ 런타임 제거
(vitaut.net){fmt} 라이브러리의 바이너리 크기 최적화
-
{fmt} 라이브러리 소개
- {fmt}는 작은 바이너리 크기로 유명한 포맷팅 라이브러리임
- IOStreams, Boost Format, tinyformat 등과 비교해 함수 호출당 코드 크기가 훨씬 작음
- 타입 소거(type erasure)를 통해 템플릿 부하를 최소화함
-
타입 소거를 통한 포맷팅
-
format
함수는vformat
함수로 작업을 위임함 - 출력 반복자와 다른 출력 타입도 특별히 설계된 버퍼 API를 통해 타입 소거됨
- 템플릿 사용을 최소화하여 바이너리 크기와 빌드 시간을 줄임
-
-
예제 코드
#include <fmt/base.h> int main() { fmt::print("The answer is {}.", 42); }
- 위 코드는 IOStreams 코드보다 훨씬 작은 크기로 컴파일됨
-
printf
와 비교해도 크기가 비슷하며, 런타임 타입 안전성을 제공함
-
바이너리 크기 최적화
- 2020년에 라이브러리 크기를 100kB 이하로 줄이는 작업을 수행함
- 최신 버전(11.0.2)의 바이너리 크기는 75kB임
- 로케일 지원을 비활성화하면 크기를 71kB로 줄일 수 있음
-
Bloaty 도구를 사용한 분석
- 숫자 포맷팅, 특히 부동 소수점 숫자 포맷팅이 큰 부분을 차지함
- 부동 소수점 지원이 필요하지 않다면 이를 비활성화할 수 있음
-
타입별 포맷팅 최적화
-
FMT_BUILTIN_TYPES
매크로를 0으로 설정하여 int 타입만 특별히 처리하고 나머지 타입은 확장 API를 통해 처리함 - 이 방법으로 바이너리 크기를 31kB로 줄일 수 있음
-
-
로케일 아티팩트 제거
-
FMT_USE_LOCALE
매크로를 사용하여 로케일 아티팩트를 제거하면 크기를 27kB로 줄일 수 있음
-
-
속도와 크기 간의 트레이드오프
-
FMT_OPTIMIZE_SIZE
매크로를 사용하여 크기를 최적화하면 바이너리 크기를 23kB로 줄일 수 있음
-
-
C++ 표준 라이브러리 의존성 제거
- 예외를 비활성화하고
-nodefaultlibs
옵션을 사용하여 C++ 런타임 의존성을 제거함 -
malloc
과free
를 사용하는 커스텀 할당자를 도입하여 바이너리 크기를 14kB로 줄일 수 있음
- 예외를 비활성화하고
-
결과 확인
-
ldd
명령어를 사용하여 C++ 런타임 의존성이 제거되었음을 확인함
-
GN⁺의 정리
- {fmt} 라이브러리는 작은 바이너리 크기와 런타임 타입 안전성을 제공하는 포맷팅 라이브러리임
- 타입 소거와 매크로 설정을 통해 바이너리 크기를 크게 줄일 수 있음
- C++ 표준 라이브러리 의존성을 제거하여 임베디드 시스템에서도 효율적으로 사용할 수 있음
- 비슷한 기능을 제공하는 라이브러리로는 IOStreams, Boost Format, tinyformat 등이 있음
Hacker News 의견
- {fmt}는 기본적으로 로케일에 독립적임
- 부동 소수점 형식화에 많은 코드가 필요함
- Dragonbox 프로젝트는 최적화된 코드로 읽어볼 가치가 있음
- C++의 기본 할당자는 malloc과 free를 사용하지 않음
- libc++의 기본 할당자가 libc의 malloc과 free를 호출하지 않는 이유를 궁금해함
- printf(Hello, World!\n")을 1008 바이트의 실행 파일 크기로 가능하게 하는 프로젝트가 있음
- 직접 비교는 어렵지만 참고할 만함
- 빈 main 함수가 있는 C 프로그램이 6kB인 시스템에서 {fmt}는 바이너리에 10kB 미만을 추가함
- 흥미로운 테스트임
- 작은 형식화 라이브러리가 문자열과 정수를 출력하는 데 약 50 바이트가 필요할 것이라고 기대했음
- 문자열은 약 4개의 명령어로 구성됨
- 정수는 약 20개의 명령어로 구성됨
- 부동 소수점은 많은 프로그램에서 사용되지 않으므로 필요할 때만 컴파일해야 함
- 마이크로컨트롤러 코드 공간이 2킬로바이트인 경우 14킬로바이트의 문자열 형식화 라이브러리를 포함하지 않음
- 이러한 생각의 틀을 벗어난 최적화가 매우 즐거움
- "14k"가 "14kB"를 의미한다는 것을 깨닫는 데 시간이 걸렸음
- fmt는 항상 문제를 일으킴
- .NET에서도 동일한 문제가 발생함
- 숫자 형식화/파싱을 많이 다루면 링커가 많은 부동 소수점 및 BigInt 관련 코드를 포함하게 되어 바이너리 크기가 커짐