0
Haskell 바이너리 크기 줄이기 — 링크 타임 최적화 두 가지 전략
backend
요약
기사 전체 정리
Haskell 바이너리 크기 줄이기 - 링크 타임 최적화 두 가지
Haskell 프로젝트는 의존성이 쌓이다 보면 바이너리가 100MB를 훌쩍 넘기기 일쑤임. GHC 9.2.5 환경에서 pandoc의 test-pandoc 바이너리로 실험한 두 가지 링크 타임 최적화 전략 공유.
전략 1: -split-sections + --gc-sections (데드 코드 제거)
GHC에 -split-sections 옵션을 주면 코드를 개별 섹션 단위로 쪼개서 emit함. 그러면 링커가 실제로 참조되지 않는 섹션(데드 코드)을 쉽게 찾아 제거할 수 있음.
cabal.project 설정:
package *
ghc-options: -split-sections
gcc-options: -fdata-sections -ffunction-sections
package pandoc
ld-options: -fuse-ld=lld -Wl,--gc-sections,--build-id- lld를 쓰는 이유는 빠르고, 아래 전략 2도 지원하기 때문
- 결과: 113M → 83M (-27%) 감소
전략 2: Identical Code Folding (ICF)
gold와 lld 모두 ICF를 지원하는데, 링크 타임에 기능적으로 동일한 섹션을 찾아서 하나로 합쳐버리는 방식임. lld의 구현이 더 효과적이라고 함.
전략 1 설정에 추가:
ld-options: -Wl,--icf=all,--ignore-data-address-equality,--ignore-function-address-equality,--print-icf-sections- 결과: 83M → 64M (추가 -23%)
- 최종 총 감소: 113M → 64M (-43%)
왜 Haskell에서 중복 코드가 많나?
같은 함수가 여러 모듈에 인라인·특수화되면서 결과적으로 동일한 바이트코드가 반복 생성되는 경우가 많음. 실험에서 ICF로 접힌 120K개 섹션 중 절반이 pandoc 자체에서 나왔음 — 단 하나의 ghc 호출에서 비롯된 것.
주의할 점 및 추가 생각
-fdistinct-constructor-tables와 충돌: 프로파일링·디버깅용 info table 중복본이 ICF에 의해 제거돼버림. 디버깅 목적으로 쓸 때는 주의 필요.- 컴파일 시간 낭비 문제: 이 중복 섹션들은 바이너리 크기만 낭비하는 게 아니라 컴파일 시간(코드 생성, simplifier 등)도 낭비하는 것. GHC가 동일하게 emit될 컴파일 유닛을 캐싱할 수 있다면 빌드 속도도 개선될 여지가 있음.
bloaty툴로 바이너리 구성을 분석하려 했지만 Haskell 코드에서 오동작함.
댓글
댓글
댓글을 불러오는 중...