buildprof는 Bun의 Zig 빌드가 Rust 빌드보다 5배 느렸던 이유를 밝혀냈다
핵심 요약
buildprof는 Bun의 Zig 빌드가 느린 이유가 막대한 Full-LTO 링킹 단계와 단일 모듈 Zig 컴파일로 인해 병렬 처리가 불가능했기 때문이며, Rust 빌드는 Thin-LTO와 여러 크레이트를 사용해 총 빌드 시간을 약 24분에서 약 5분으로 단축했다는 것을 입증했다.
buildprof란?
buildprof는 Linux용 오픈소스 추적 도구(https://github.com/lalitMaganti/buildprof)로, 빌드 과정에서 생성되는 모든 프로세스, 파일 읽기/쓰기, 그리고 선택적 컴파일러 내부 추적 정보를 기록한다. 이 도구는 시간이 왼쪽에서 오른쪽으로 흐르는 계층적 타임라인을 렌더링하며, 막대 너비는 지속 시간을, 자식 프로세스는 부모 프로세스 아래에 표시된다. 빌드 명령어 앞에 다음과 같이 붙여 사용한다:
buildprof -- make -j16
buildprof -- cargo build
buildprof -- ninja -C out/target
buildprof -- just build
buildprof -- ./dev/custom-build-script.sh
기록된 데이터는 Perfetto 플랫폼 기반의 웹 UI(https://buildprof.lalitm.com/)에서 탐색할 수 있다.
Bun의 컴파일 시간을 조사한 이유?
Jarred Sumner(Bun의 수석 아키텍트)가 트윗을 통해 Bun 1.4.0(Rust 기반)이 Linux에서 Bun 1.3.14(Zig 기반)보다 5배 빠르게 컴파일되었다고 주장했다. 트윗에서는 Zig 빌드가 Full LTO를 사용한 반면, Rust 빌드는 Thin LTO를 사용했다고 언급했다. LTO 전략은 빌드 시간에 큰 영향을 미칠 수 있으므로, 저자는 이 주장을 검증하고 근본 원인을 이해하기 위해 buildprof를 개발했다.
수치 재현
저자는 Bun 1.3.14와 1.4.0을 체크아웃하고, 6코어/12스레드 VM에서 CI 빌드 스크립트를 작성하여 트윗과 유사한 시간을 얻었다:
| 빌드 | 보고된 CI 중앙값 | 단일 VM에서 재현된 결과 |
|---|---|---|
| Zig 시대 (Bun 1.3.14) | 30분 06초 | 24분 24초 |
| Rust 시대 (Bun 1.4.0) | 5분 37초 | 5분 40초 |
차이가 여전히 지속되자 더 깊이 조사하게 되었다.
빌드를 프로세스 트리로 시각화하기
빌드는 본질적으로 프로세스 트리이다: 최상위 명령어(예: cargo)가 컴파일러, 링커, 스크립트를 생성하고, 이들 중 일부는 더 많은 프로세스를 생성할 수 있다. ptrace를 사용해 각 fork/exec/exit 이벤트를 기록하고, seccomp 필터를 통해 파일 I/O를 가로채는 방식으로 buildprof는 완전한 트리를 구성하고 노드 간 파일 의존성을 매핑한다. 이 접근법은 빌드 시스템에 관계없이 적용 가능하며, 사용자 정의 스크립트도 자동으로 포함한다.
Zig 빌드: 링커의 성능 저하
Zig 시대 CI 빌드의 타임라인은 단일 ld.lld 호출이 약 16분(총 시간의 약 2/3)을 차지하고 있음을 보여준다. --compiler-traces를 활성화하면 내부 LLD 단계를 확인할 수 있으며, OptModule 단계만으로도 10분 이상 소요됨을 확인할 수 있다. 이는 Full LTO가 링커에게 전체 프로그램에 대해 무거운 최적화 단계를 수행하도록 강제함을 의미한다.
Rust 빌드: 얇은 링킹
Rust 시대 CI 빌드의 타임라인은 총 링킹 시간이 2분 24초임을 보여준다. 링커 명령어에는 -plugin-opt=thinlto가 포함되어 있어, 링커는 가벼운 심볼 해결만 수행하고 대부분의 최적화는 이전에 병렬로 실행된 rustc 호출에서 이미 완료된다. 이 극단적인 차이는 LTO 전략이 시간 차이의 주요 원인임을 시사한다.
실험: Zig를 ThinLTO로 전환
저자는 Bun의 Zig 빌드를 ThinLTO로 수정하고 새로운 빌드를 기록했다. 링킹 시간은 3분 40초 개선되었지만, 전체 빌드 시간은 여전히 약 13분이 걸렸다. 그 이유는 여전히 Full LTO로 빌드된 많은 WebKit 라이브러리들(예: libJavaScriptCore.a)이 있었기 때문이다. 이러한 사전 빌드된 아카이브는 링커가 대규모 비트코드 모듈에 대해 전체 최적화를 수행하도록 강제했다.
WebKit을 ThinLTO로 재빌드하기
필요한 WebKit 버전과 ICU 종속성을 ThinLTO로 재빌드하고 다운로드된 아카이브를 교체한 결과, 다음과 같은 결과를 얻었다:
| 구성 | 전체 빌드 시간 | 최종 링커 시간 |
|---|---|---|
| 원본 Full LTO (Zig) | 24분 24초 | 16분 35초 |
| Zig ThinLTO + 원본 WebKit | 20분 20초 | 12분 55초 |
| Zig ThinLTO + 재빌드된 ThinLTO WebKit | 15분 11초 | 7분 22초 |
재빌드된 WebKit은 링킹 시간을 절반으로 줄였지만, 여전히 Rust 버전보다 느렸다.
병렬 처리: 크레이트 vs 단일 모듈
의존성 화살표를 살펴보면, C++ 측이 먼저 완료된 후에도 링커가 bun-zig.o를 기다리는 것을 확인할 수 있다. 중요한 관찰은: Bun의 Rust 빌드는 90개 이상의 크레이트로 나뉘어 있어 Cargo가 많은 rustc 프로세스를 병렬로 실행할 수 있다는 점이다. 반면 Zig 빌드는 전체 코드베이스를 단일한 거대한 Zig 모듈로 컴파일하여 병렬 처리를 제한하고 링커의 부담을 증가시켰다(단일 큰 ThinLTO 비트코드 모듈 대비 여러 작은 모듈).
기타 발견사항
- 다양한 CI 단계 – 추적은 네트워크 프로브, Docker 검사, Git 쿼리 등을 포착했으며, 각각 1초 미만 소요됨.
- 냉각된 종속성 다운로드 – 처음으로 WebKit을 다운로드/추출하는 데 약 20초가 소요되었으며,
node → tar → gzip이벤트로 확인 가능. - C++ 컴파일 세부 정보 – Clang에
-ftime-trace를 활성화하면 12초 컴파일 시간이 프론트엔드와 백엔드에 균등하게 나뉘며,ModuleInlinerWrapperPass가 4초 이상 소요됨을 확인할 수 있다.
buildprof의 동작 방식
- 기록 –
ptrace를 사용해 프로세스 분기/실행을 추적하고, seccomp 필터를 통해 파일 이벤트를 가로챈다. 이는 eBPF나 ftrace와 같은 도구의 권한 및 안정성 문제를 피한다. - 오버헤드 – 주로 파일 열기 수에 비례한다. 예: ripgrep(약 0.2초), Redis(약 5초).
--no-file-events플래그를 사용하면 파일 추적 오버헤드를 제거할 수 있다. - UI – Perfetto UI의 플러그인 시스템을 기반으로 하며, 타임라인 렌더링, 프로세스 트리 레이아웃, 생산자와 소비자 간의 선택적 화살표를 제공한다.
관련 도구 및 그 한계
| 도구 | 강점 | 한계 |
|---|---|---|
| ninjatracing | Ninja 엣지와 병렬 처리를 보여줌 | 래퍼 스크립트와 하위 프로세스 트리를 놓침 |
| Cargo timings | Cargo 단계별 시간을 상세히 제공 | 사용자 정의 스크립트와 비-Cargo 작업 무시 |
clang -ftime-trace |
컴파일러 내부 단계를 제공 | 전체 빌드 프로세스를 보여주지 않음 |
| strace / tracexec | 일반적인 시스템 호출 추적 | 빌드 중심이 아니며 타임라인 해석이 어려움 |
| What the Fork | 프로세스 트리 시각화 | 개인 베타, 오픈소스 아님 |
buildprof는 전체 프로세스 트리 캡처, 파일 의존성 화살표, 선택적 컴파일러 추적을 하나의 인터랙티브 타임라인에 통합하여 이 공백을 메운다.
향후 방향성
- 파일 시스템 추적 오버헤드 감소(특히 많은 파일을 열 수 있는 빌드에 초점)
- macOS 및 Windows 지원 추가
- 더 많은 빌드 시스템 지원(예: npm, Gradle, Bazel)
- 임계 경로(critical paths) 자동 계산 및 표시
- GitHub 이슈를 통한 사용자 피드백 통합
커뮤니티 반응 (선택적 HN 댓글)
"좋은 글입니다! 저자는 Bun의 Zig 빌드를 Rust보다 훨씬 빠르게 만들기를 기대했지만, 깊이 있는 분석이었네요" – @anaqin
"Electric Insight처럼 보입니다… 빌드를 비교해 왜 하나가 더 느린지 볼 수 있어요" – @t43562
"좋아요! macOS 버전은 어떻게 되나요?" – @jiehong
"WebKit 링킹에서 Full LTO가 시리얼 부분이에요. 시각화 도구는 링킹 시간과 코드 생성 시간을 분리해 보여주나요?" – @xcc3641
이 댓글들은 도구가 이 사례 연구를 넘어서 적용 가능성을 보여주고 있으며, 크로스 플랫폼 지원에 대한 관심을 보여준다.
결론
- Bun의 Zig 빌드 지연의 주요 요인은 Full-LTO 링커 단계였으며, 이는 약 16분간 단독으로 실행되었다.
- ThinLTO로 전환하면 링킹 시간이 줄었지만, 사전 빌드된 WebKit 아카이브가 여전히 Full-LTO이기 때문에 큰 차이가 남.
- WebKit을 ThinLTO로 재빌드하면 전체 빌드 시간이 15분으로 줄었지만, Rust 빌드보다 여전히 느렸다. 그 이유는 90개 이상의 크레이트를 통해 대규모 병렬 처리가 가능했기 때문이다.
- buildprof는 숨겨진 성능 저하를 드러내고 가설을 확인하며 구체적인 최적화를 안내하는 데 매우 유용했다.
느린 빌드에 직면했다면, buildprof를 시도해 보세요: buildprof -- <your-build-command>를 입력하고 https://buildprof.lalitm.com/에서 타임라인을 탐색하세요.
Sources
관련
- 프로젝트
- Dispatch
- Dispatch
- Dispatch
- 프로젝트