TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 20 READS

빌드가 왜 느린가: 프로세스 트리로 Bun 컴파일 시간을 파헤친 buildprof

빌드가 왜 느린가: 프로세스 트리로 Bun 컴파일 시간을 파헤친 buildprof
SOURCE IMAGE · HACKER NEWS

소프트웨어 빌드가 느릴 때 우리는 흔히 "코드가 많으니 어쩔 수 없다"고 넘긴다. 그러나 실제로는 병렬화 실패, 반복 작업, 의존성 다운로드, 지나치게 비대한 컴파일러·링커 호출처럼 고칠 수 있는 원인이 숨어 있는 경우가 더 많다. Perfetto 개발에 참여하는 한 엔지니어가 만든 오픈소스 추적 도구 buildprof는 바로 이 지점을 겨냥한다. 기존에 쓰던 빌드 명령 앞에 buildprof --를 붙이기만 하면, 빌드가 실행한 모든 프로세스와 그 하위 프로세스를 하나의 타임라인 위에 펼쳐 보여준다. 시간은 왼쪽에서 오른쪽으로 흐르고, 막대의 너비는 실행 시간을, 아래로 뻗은 막대는 부모 프로세스가 낳은 자식 프로세스를 나타낸다.

왜 프로세스 단위인가

cargo buildzig build를 입력하면 마치 한 프로그램이 도는 것처럼 느껴지지만, 빌드 시스템 자체는 컴파일러·코드 생성기·아카이버·링커·각종 스크립트를 불러오고 그것들이 다시 다른 프로그램을 실행한다. Cargo는 크레이트로, Ninja는 빌드 엣지로, CMake는 또 다른 빌드 시스템용 지시로 각자 다르게 작업을 표현하지만, 운영체제 관점에서 보면 대체로 '프로세스가 프로세스를 실행하는' 동일한 그림이다. buildprof는 이 공통 계층에서 각 하위 프로세스의 시작과 종료를 기록해 타임라인으로 만든다. 도구별 개념 차이에 얽매이지 않고 빌드 전체를 가로질러 볼 수 있다는 점이 핵심이다.

이 도구가 만들어진 계기는 Bun 런타임의 수석 아키텍트 Jarred Sumner의 트윗이었다. 새 Rust 빌드가 리눅스에서 기존 Zig 빌드보다 5배 이상 빠르다는 주장인데, 저자는 비슷한 복잡도라면 Zig가 Rust보다 빨리 컴파일되곤 했던 자신의 경험과 배치돼 의문을 품었다. 트윗에는 쉽게 지나칠 수 있는 단서도 있었다. Zig 빌드는 Full LTO를, Rust 빌드는 ThinLTO를 썼다는 것이다. 링크 타임 최적화(LTO)는 컴파일 단위 경계를 넘어 최적화를 수행하는데, Full LTO는 단위를 하나의 거대한 작업으로 합치는 반면 ThinLTO는 분리를 유지해 상당 부분을 병렬로 처리한다.

16분을 잡아먹던 링커의 정체

저자는 Bun 1.3.14와 1.4.0을 체크아웃해 리눅스 x64 CI 빌드를 6코어·12스레드 VM에서 재현했고, Jarred가 말한 격차를 확인했다. buildprof로 Zig 빌드를 기록하자 ld.lld 링커 호출이 빌드 끝에서 홀로 16분 넘게, 전체의 약 3분의 2를 차지하는 모습이 곧바로 드러났다. --compiler-traces 옵션으로 LLD 내부 타이밍을 넣어 보니 시간의 대부분이 LTO에서 소모됐고, 기계어를 생성하는 패스를 포함한 OptModule 구간만 10분을 넘겼다. 반면 Rust 빌드의 링크는 2분 24초에 불과했고 명령줄에는 예상대로 -plugin-opt=thinlto가 있었다.

Zig 빌드 플래그를 ThinLTO로 바꾸자 링크는 3분 40초 빨라졌지만 여전히 13분 가까이 걸렸다. 원인을 추적하니 상당수 작업이 이름에 JSC가 붙은 함수, 즉 Bun이 자바스크립트 실행에 쓰는 JavaScriptCore 엔진에 몰려 있었다. Bun은 이 WebKit 라이브러리를 직접 빌드하지 않고 별도 WebKit 빌드에서 내려받고 있었는데, 그 빌드의 플래그가 또 -flto=full이었다. Rust 빌드는 ThinLTO를 택한 더 새 WebKit 리비전을 썼던 것이다. 저자가 해당 리비전과 ICU 의존성을 ThinLTO로 다시 빌드해 교체하자 링크는 7분 22초까지 줄었다.

진짜 원인은 모듈 구조였다

그럼에도 빌드는 15분이 걸렸고, 링커가 시작되기까지 8분 가까이가 그냥 흘러갔다. buildprof는 각 프로세스가 읽고 쓴 파일도 기록해 생산자와 소비자를 화살표로 잇는데, 이를 켜자 링커가 C++ 컴파일 산출물과 Zig가 만든 bun-zig.o를 기다리느라 Zig 분기가 끝날 때까지 시작조차 못 한다는 사실이 보였다. 결정적 차이는 여기서 명확해졌다. Rust에서 Bun은 90개 넘는 크레이트로 쪼개져 있지만, Zig에서는 전체가 하나의 모듈로 컴파일되고 있었다. 단일 모듈은 Rust처럼 병렬화할 수 없고, 저자는 이것이 거대한 ThinLTO 비트코드 하나를 링커가 통째로 최적화해야 하는 느린 링크의 원인이기도 하리라 추정했다. 다만 이를 증명하려면 Zig 모듈을 직접 분할해야 했고, 어차피 폐기될 코드라 판단해 여기서 멈췄다.

기술적으로 buildprof는 디버거가 쓰는 것과 같은 리눅스 ptrace 인터페이스로 프로세스의 fork·exec·exit를 따라가고, 파일 활동은 필요한 호출만 가로채는 seccomp 필터로 잡는다. eBPF는 CAP_BPF·CAP_PERFMON 권한과 불안정한 커널 훅이, ftrace는 추적 인스턴스와 필터 관리가 걸림돌이어서 ptrace가 가장 적합했다는 설명이다. 오버헤드는 빌드가 여는 파일 수에 좌우되는데 ripgrep은 거의 영향이 없었고 파일을 자주 여는 Redis는 약 5초가 더해졌으며, 부담되면 --no-file-events로 파일 추적을 끌 수 있다. UI는 Perfetto UI의 소프트 포크로, 추적 파싱과 타임라인 렌더링 같은 무거운 작업은 Perfetto가 맡고 빌드에 특화된 표현만 플러그인으로 얹었다.

한국의 실무자에게 이 사례가 주는 함의는 분명하다. 빌드가 느릴 때 언어나 컴파일러를 성급히 탓하기 전에, 시간이 실제로 어디로 새는지 프로세스 수준에서 관찰해야 한다는 것이다. Bun 사례에서 '언어 교체'로 요약된 5배의 개선은 실은 Full LTO 대 ThinLTO 설정, 다운로드하는 WebKit 라이브러리의 빌드 플래그, 그리고 무엇보다 코드를 다수 단위로 쪼개 병렬화한 구조 변경이 겹친 결과였다. 참고로 저자는 빌드 도중 외부 인터넷에 장비 IP를 묻고 실행 중인 Docker 컨테이너를 살피며 최신 Git 커밋 메시지를 읽는 명령까지 발견했는데, 시간은 1초도 안 되지만 빌드 추적에서 이런 예기치 못한 동작이 드러난다는 점 자체가 관찰 도구의 가치를 보여준다. 다만 이 분석은 특정 하드웨어에서의 CI 빌드 재현에 기반하며, 단일 모듈이 링크 지연의 원인이라는 추정은 증명되지 않은 가설로 남았다는 한계도 함께 기억할 필요가 있다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://lalitm.com/post/buildprof/
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...