본문으로 건너뛰기
0
r/jeffnews HN 약 4분

Haskell 바이너리 크기 줄이기 — 링크 타임 최적화 두 가지 전략

backend

요약

GHC 9.2.5 환경에서 pandoc 바이너리를 대상으로 -split-sections/--gc-sections와 Identical Code Folding(ICF) 두 가지 링크 타임 기법을 적용해 113M → 64M(-43%)으로 줄인 실험 공유.

기사 전체 정리

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 코드에서 오동작함.

핵심 포인트

  • -split-sections + --gc-sections로 데드 코드 제거: 113M → 83M (-27%)
  • lld의 ICF(--icf=all)로 동일 섹션 병합: 83M → 64M (-23% 추가)
  • ICF로 접힌 120K 섹션 중 절반이 pandoc 단일 ghc 호출에서 발생
  • -fdistinct-constructor-tables와 ICF가 충돌해 디버깅용 info table이 제거될 수 있음
  • 중복 섹션은 바이너리 크기뿐 아니라 컴파일 시간도 낭비 — GHC 캐싱 개선 여지 있음

인사이트

Haskell 특성상 인라인·특수화로 동일 코드가 여러 모듈에 반복 생성되는 구조적 원인이 있어 ICF 효과가 특히 큼. 단순 옵션 추가만으로 43% 감소는 인상적이며, GHC 컴파일러 레벨의 중복 캐싱 최적화로 이어질 수 있는 흥미로운 방향을 제시함.

댓글

댓글

댓글을 불러오는 중...

backend

Redis 8.0 출시 — I/O 스레딩 갈아엎고 처리량 3배, 2.1M ops/sec 달성

Redis 8.0이 I/O 스레딩 모델을 완전히 재설계해서 16코어 기준 2.1M ops/sec를 달성함 (7.4 대비 3배). Hash field expiration, Vector search HNSW, Client-side caching v2, Redis Functions 2.0 async 실행 등 굵직한 기능이 추가되고, jemalloc 통합으로 메모리 fragmentation도 25% 줄어듦.

backend

Go 1.26의 타입 생성(Type Construction)과 순환 감지(Cycle Detection) 개선

Go 1.26에서 타입 체커의 타입 생성 알고리즘을 개선해 재귀 타입과 배열 크기 계산 시 발생하던 순환 감지 문제를 체계적으로 해결했다. 불완전한 값이 다운스트림으로 퍼지기 전에 업스트림에서 차단하는 새로운 접근법으로 여러 컴파일러 패닉을 수정.

backend

Cloudflare Gen 13 서버: 캐시를 코어로 바꿔 성능 2배 달성한 이야기

Cloudflare가 AMD Turin 9965(192코어) 기반 Gen 13 서버를 배포함. 코어당 L3 캐시가 6배 줄어 레거시 NGINX 스택(FL1)으로는 레이턴시 50% 악화가 불가피했으나, Rust로 전면 재작성한 FL2로 전환해 Gen 12 대비 처리량 2배, 성능/와트 50% 개선을 달성함.

backend

칩셋 레이턴시를 측정해봤더니 — 쓸모는 없지만 재밌는 실험

Vulkan GPU 벤치마크로 여러 세대 마더보드 칩셋의 PCIe 레이턴시를 측정한 실험. CPU 직결 대비 칩셋 경유 시 수백 ns 레이턴시가 추가되며, 의외로 2012년 Skylake Z170이 가장 낮은 추가 레이턴시를 보임.

backend

ForgeKV — Rust로 만든 멀티코어 Redis 대체제

Rust로 만든 Redis 드롭인 대체제. 64-샤드 잠금 아키텍처로 멀티코어 스케일링 지원. 2코어 환경에서 Redis 7 대비 41% 빠른 SET 처리량(158K ops/s). 고동시성에서는 약점 있음. Source-available 라이선스.