1P by GN⁺ 13시간전 | ★ favorite | 댓글 1개
  • 러스트로 만든 웹사이트를 Docker로 반복 빌드할 때 빌드 시간 문제를 겪음
  • 기본 Docker 설정에서는 전체 의존성 재빌드가 매번 발생해 4분 이상 소요됨
  • cargo-chef와 캐싱 도구를 사용해도 최종 바이너리 빌드에 여전히 많은 시간이 걸림
  • 프로파일링 결과, LTO(링크 타임 최적화)와 LLVM 모듈 최적화에 대부분 시간을 소모함
  • 최적화 옵션, 디버그 정보, LTO 설정을 조절해 일부 개선 가능하지만, 최종 바이너리 컴파일에 최소 50초는 소요되는 현상 확인

문제 제기 및 배경

  • Rust로 만든 개인 웹사이트를 수정할 때마다 정적 링크 바이너리를 빌드해 서버에 복사 후 재시작하는 번거로운 작업이 반복됨
  • Docker 또는 Kubernetes 등 컨테이너 기반 배포로 전환하려 했으나, Rust의 Docker 빌드 속도가 큰 문제로 드러남
  • Docker 내에서 작은 코드 변경에도 전체를 처음부터 재빌드해야 해서 비효율적인 상황 발생

Docker에서 Rust 빌드 – 기본 접근

  • 일반적인 Dockerfile 접근은 모든 의존성과 소스코드를 복사한 뒤 cargo build 실행 방식임
  • 이 경우 캐싱의 이점이 없어 전체 재빌드가 반복됨
  • 본인의 웹사이트 기준, 전체 빌드는 약 4분 소요됨—의존성 다운로드에도 추가 시간 소요

Docker 빌드 캐싱 개선 – cargo-chef

  • cargo-chef 도구를 활용하면, 의존성만 별도의 레이어로 미리 캐시해둘 수 있음
  • 이를 통해 코드 변경 시 의존성 빌드가 재사용되어 빌드 속도 개선 효과 기대
  • 실제 적용 시, 전체 시간 중 25%만이 의존성 빌드에 집중되고, 최종 웹서비스 바이너리 빌드에 여전히 상당한 시간이 소요됨(2분 50초~3분)
  • 주요 의존성(axum, reqwest, tokio-postgres 등)과 7,000여 줄의 자체 코드로 구성됨에도 rustc의 단일 실행에 3분 소요되는 구조

rustc 빌드 시간 분석: cargo --timings

  • cargo --timings를 사용하여 각 크레잇(compilation unit)별 빌드 시간 확인 가능
  • 결과적으로 최종 바이너리 빌드가 전체 시간의 대부분을 차지하는 것을 확인
  • 보다 세밀한 원인 분석에는 도움이 되나, 구체적 컴파일러 내부 동작 파악이 부족함

rustc 자체 프로파일링(-Zself-profile) 활용

  • rustc의 자체 프로파일링 기능을 -Zself-profile 플래그로 활성화하여 세부 동작 시간 측정
  • 이를 위해 환경변수를 통해 프로파일링 활성화
  • 결과 요약(summarize) 툴로 분석 시, LLVM LTO(링크 타임 최적화), LLVM 모듈 코드 제너레이션이 전체 시간의 60% 이상 차지함을 발견
  • flamegraph 시각화로도 codegen_module_perform_lto 단계에서 전체 시간의 80% 소모를 확인

LTO(링크 타임 최적화)와 빌드 최적화 옵션

  • Rust 빌드는 기본적으로 codegen unit별로 분할된 후, LTO에 의해 전체 최적화가 비교적 후반에 적용됨
  • LTO에는 off, thin, fat 등 여러 옵션이 존재: 각각 성능 및 최종 결과물에 영향
  • 작성자의 프로젝트는 Cargo.toml에서 LTO를 thin으로, 디버그 심볼도 full로 설정된 상태였음
  • 다양한 LTO/디버그 심볼 조합을 테스트한 결과:
    • full 디버그 심볼의 빌드 시간 증가 효과와 fat LTO의 4배 가량 빌드 지연 확인
    • LTO 및 디버그 심볼 제거 시에도 최소 50초 빌드 시간 필요

추가 최적화 및 단상

  • 50초 정도면 실제 서비스 부하가 거의 없는 본인 사이트에는 큰 문제 없으나, 기술적 호기심으로 추가 분석 시도
  • 증분 컴파일(incremental compilation)을 Docker로 잘 활용하면 더 빠른 빌드도 가능하나, 빌드 환경 청결성과 도커 캐시의 결합 필요

LLVM 단계 세부 프로파일링

  • LTO와 디버그 심볼 제거 후에도 LLVM_module_optimize 단계에서 70% 가까운 시간 소모
  • release 프로필에서 opt-level 기본값(3)으로 인한 최적화 비용이 큼을 인지, 바이너리에서만 opt-level을 낮추는 방법 테스트
  • 각종 최적화 조합 실험 결과 최적화 미적용(opt-level=0) 시 15초 내외, 최적화 적용(1~3) 시 50초 내외로 소요됨

LLVM 추적 이벤트 심층 분석

  • rustc의 추가 플래그(-Z time-llvm-passes, -Z llvm-time-trace)를 이용해 LLVM 단계별 실행 시간 상세 추적 가능
  • -Z time-llvm-passes는 출력이 방대해 Docker의 log 제한을 초과하는 경우가 많아 log 설정 조정 필요
  • 로그를 파일로 저장해 분석하면 각 LLVM 최적화 패스별 실행 시간을 개별적으로 확인 가능
  • -Z llvm-time-trace 옵션은 chrome tracing 포맷의 방대한 JSON 출력을 생성하며, 파일이 매우 커 일반적인 텍스트 편집/분석 도구를 사용하기 어려움
  • 이를 newline 단위로 분할 처리(jsonl)해 CLI/스크립트 환경에서 분석 가능

주요 insight 및 결론

  • Rust로 복잡한 프로젝트를 Docker로 빌드할 때, 빌드 속도 병목은 주로 최종 바이너리 빌드 및 관련 LLVM 최적화 단계에서 발생함
  • LTO와 디버그 심볼, opt-level을 조정할 때 빌드 시간과 바이너리 크기 간 트레이드오프 분명함
  • 최적화 옵션을 적극적으로 조정할 경우 빌드 시간 대폭 단축 가능하나, 최적화 미사용 시 퍼포먼스 저하 가능성 존재
  • 대규모 크레이트 의존성과 상용 환경에서 빌드 효율 중요시 된다면 프로파일링을 적극적으로 활용해 세부 병목을 구체적으로 파악하는 것이 좋은 전략임
  • 러스트 빌드 파이프라인 설계 시 LTO, opt-level, 디버그 심볼, 캐시 전략에 대한 정교한 조합 설계가 필요함
Hacker News 의견
  • 러스트 프로젝트가 종종 겉으로 보기에 작아 보여 흥미로움 느껴짐. 첫째, 의존성은 코드베이스의 실제 크기와 연결되지 않음. C++에선 대형 프로젝트 의존성을 종종 베닝(vendoring)하거나 아예 안 쓰기도 해서, 40만 줄 코드 중 느린 게 많으면 "코드 많으니 느릴 만하지"라고 생각할 수 있음. 둘째, 훨씬 더 문제되는 부분은 매크로임. 10줄, 100줄씩 반복 확장되는 매크로는 1만 줄 프로젝트도 금세 백만 줄로 만들어버릴 수 있음. 셋째는 제네릭임. 제네릭 인스턴스화마다 CPU 리소스를 소모함. 그래도 변명 좀 하자면, 이런 기능들 덕에 C로 10만 줄, C++로 2만 5천 줄인데 러스트로는 수천 줄로 줄어드는 장점 있음. 다만 이런 기능이 과도하게 쓰이면서 생태계가 느리게 보이는 것도 사실임. 예를 들어 우리 회사에서 async-graphql을 쓰는데, 라이브러리 자체는 훌륭하지만 프로시저 매크로 의존도가 심함. 성능 관련 이슈가 수년간 오픈돼 있고, 데이터 타입 추가할 때마다 컴파일러가 확실히 느려짐을 체감함

    • 작은 C 유틸리티처럼 원래 코드가 단순했던 곳을 러스트로 다시 작성하는 경우가 많은 이유가 궁금함. 10만 줄짜리 대형 C 프로그램 러스트 포팅 사례보다 더 자주 보게 되는 건 아주 작은 규모 코드임. 작은 프로그램의 컴파일 속도에서 러스트와 C가 어떻게 비교되는지 궁금함. 프로그램 크기가 아니라 컴파일 속도가 궁금한 것임. 참고로 최근 측정상, 러스트 컴파일러 툴체인 크기가 내가 쓰는 GCC보다 약 2배 큼. 1. 이 정도 작은 프로그램들은 어떤 언어든 메모리 안전성 이슈가 숨어 있을 가능성이 낮고, 규모도 작아서 감사도 쉬움. 10만 줄 C 프로그램과 상황이 다름
    • 타입을 새로 정의할 때마다 컴파일러가 느려지는 걸 피부로 느낄 수 있음. 참고로 컴파일러 성능이 타입의 “깊이”에 따라 지수적으로 느려지는 것으로 기억함. GraphQL 같은 경우 중첩 타입이 많아 이 문제가 특히 심각함
    • 매크로가 몇십 혹은 몇백 줄씩 확장하면 코드베이스가 기하급수적으로 커질 수 있다는 문제에 대응해 최근 분석 도구 지원이 추가됨. 관련 자료 참고: https://nnethercote.github.io/2025/06/…
  • Ryan Fleury가 Epic RAD Debugger를 C로 27만8천 줄짜리 유니티 빌드 방식(모든 코드가 하나의 파일로 단일 컴파일 유닛)으로 만들었고, 윈도우에서 클린 컴파일 시 1.5초밖에 안 걸림. 이 사례만 봐도 컴파일이 엄청 빠를 수 있다는 걸 보여주는데, Rust나 Swift에서도 비슷하게 못 만드는 이유가 궁금함

    • 빌드 타임에 컴파일러가 해주는 일이 많아질수록 빌드 시간이 길어짐. Go는 대규모 코드베이스도 1초 이하 빌드 타임 달성 가능함. 빌드 때 필요한 작업만 최소화해둔 간단한 모듈 시스템과 타입 시스템, 그리고 대부분 기능을 런타임 GC에 맡김. 반대로 매크로나 복잡한 타입 시스템, 높은 수준의 견고함이 요구되면 빌드 타임이 길어질 수밖에 없음
    • 러스트 역시 빌드 단위가 크레이트 전체이고, 컴파일러가 LLVM IR로 적절한 사이즈로 쪼갬. 중복 작업과 인크리멘탈 빌드 밸런스도 알아서 조정함. 러스트가 소스 코드 라인 기준으론 C++보다 빌드가 더 빠른 경우가 많음. 다만 러스트 프로젝트는 의존성까지 모두 컴파일하는 특성 있음
    • Rust와 Swift가 C 컴파일러보다 컴파일이 느린 이유는, 언어 자체가 훨씬 많은 분석 작업 필요함. 예를 들어 러스트의 borrow checker가 무료로 제공되는 게 아님. 컴파일 타임 체크만 해도 상당한 리소스 소모임. C가 빠른 건 기본적인 문법 이상 검사 안 하다시피 하기 때문임. 오히려 C는 foo(char*)에 foo(int)를 호출하는 이상한 조합도 체크 안 함
    • 2000년대에 수만 줄짜리 C++ 프로젝트를 컴파일했는데, 구형 컴퓨터에서도 1초 이내로 빌드 끝남. 반면 Boost만 쓴 HELLO WORLD는 몇 초 걸림. 결국 빌드 속도는 언어나 컴파일러만이 아니라, 코드 구조와 사용 기능에 따라 크게 달라짐. C 매크로로 DOOM 만들 수도 있겠지만 아마 빠르진 않을 것임. 반대로 러스트도 빌드 빠르게 구조화 가능함
    • C와 Go 등 빠른 컴파일을 지향하는 언어가 빠른 건 별로 신기하지 않음. 진짜 어려운 건 러스트의 의미론을 빠르게 컴파일하는 것임. 이 문제는 러스트 공식 FAQ에도 있음
  • 나는 Go가 최적화보다 컴파일 속도를 우선시한 게 참 다행. 서버, 네트워킹, glue 코드 작업에는 컴파일이 정말 빠른 게 무엇보다 중요함. 타입 안정성도 적당히 원하지만, 느슨하게 프로토타이핑하게 방해하지 않는 선을 원함. GC가 있다는 점도 편리함. 구글에서 개발 대규모화 경험 끝에 단순한 타입, GC, 엄청 빠른 컴파일이 실행 속도나 의미론적 완벽성보다 훨씬 중요하단 결론을 냈다고 생각함. Go로 만들어진 대규모 네트워킹·인프라 소프트웨어 사례만 봐도 선택이 완전 적중. 물론 GC 허용 불가 환경이나 완벽한 정확도가 더 중요한 곳에선 Go 안 쓸 수 있지만, 내 작업 환경엔 Go의 선택이 최적임

    • 나도 Go를 좋아하지만, 이 언어가 조직적 구글의 대단한 집단 지성의 산물이라고는 생각하지 않음. 구글 경험이 녹아 있었다면 예를 들면 널 포인터 예외 정적 제거 같은 기능은 추가했겠지. 오히려 몇몇 구글 개발자들이 본인이 원하던 언어를 만든 결과물 같음
    • 빠른 컴파일, 적당한 타입 시스템, GC 같은 Go의 장점이 있지만, 디자인 공간상 이미 Java가 비슷한 자리를 차지하고 있었음. Go가 만들어진 건 단순히 창작 욕구에서 비롯된 게 주요했던 것 같고, 결국 오리지널 타깃(서버 측 C/C++/Java)보다도 스크립트 언어(Python/Ruby/JS) 사용자 층에서 더 흡수된 느낌임. 스크립트 유저들은 쉽고 빠른 타입 시스템만 원했고, Java는 너무 올드하고 재미 없었음. 이미 Java는 서버/컨퍼런스/라이브러리 분야엔 공간이 없었음
    • 구글 개발자가 C++ 프로젝트 컴파일 기다리며 Go 디자인했다는 얘기도 있음
    • "obnoxious type"이 뭐냐 묻고 싶음. 타입은 데이터를 올바르게 표현하든가 못 하든가일 뿐이고, 실제론 어떤 언어에서도 타입 체크러를 억지로 조용히 시킬 수 있음
    • Go의 설계 목적과 실제 용도에 딱 맞는 언어임. 가장 큰 위험은 병렬처리와 mutable 상태를 채널로 공유하는 방식인데, 이 부분에서 미묘하거나 취약한 버그가 발생 가능함. 보통 대부분의 사용자는 이런 패턴을 쓰지 않음. 난 Rust를 사용하는데, 작업 자체가 느린 알고리즘을 느린 하드웨어에 최대한 짜내서 올려야 하는 상황임. 덕분에 대규모 병렬화가 아주 미묘하게 불가능한 문제임
  • 단일 스태틱 바이너리 설치가 컨테이너 관리보다 단순하다는 주장을 이해 못 하겠음

    • docker가 실제로 어떤 작업을 하는지 명확히 이해하지 않은 것 같음. 예를 들어 "docker 이미지로 배포하면 매번 전체를 새로 빌드한다"고 했는데, 사내 빌드/배포 환경에선 이런 문제 없어도 됨. 개인 용도라면 개발 편의성 유지한 채 로컬에서 빌드한 파일만 컨테이너에 넣어도 무관함. 빌드 환경 흔적 경로만 신경쓰면 됨. CI/CD나 단체 프로젝트에선 어디서든 0부터 빌드 생성을 보장하는 것에 방점이 찍히지만, 개인 작업은 그럴 필요 없음
    • 원문에서 목표는 단순화가 아니라 현대화임. "최근 10년간 대부분의 소프트웨어가 컨테이너 배포를 표준으로 삼고 있으니 내 웹사이트도 docker, kubernetes 같은 컨테이너로 배포하겠다"고 한 것. 컨테이너는 프로세스 격리, 보안, 표준화된 로깅, 수평 확장성 등 여러 이점 있음
  • 내 노트북(Mac M4 Pro)에서 Deno(대형 러스트 프로젝트) 전체 컴파일에 2분 걸림. 커맨드 기준으로 보면, debug는 약 1분 54초, release는 약 8분 17초 소요됨. 인크리멘탈 컴파일 없이 측정한 수치임. 사실 배포 빌드는 CI/CD 시스템에서 돌아가니, 직접 기다릴 필요 없음

    • M1 Max 기준 6분, M1 Air 기준 11분 정도 걸렸다는 관련 기사 있음
  • Cranelift 얘기는 어디에 나오지? 내 생각엔 러스트로 게임 개발하다가 컴파일 시간이 너무 길어 거의 포기할 뻔함. 조사해보니 LLVM이 최적화 레벨과 무관하게 느림. Jai언어 개발자들이 늘 지적했던 것임. Cranelift로 빌드 타임이 16초에서 4초로 단축되는 경험도 했음. Cranelift 팀에 감탄!

    • 최근 Bevy game jam에서 Dioxus 커뮤니티에서 나온 'subsecond'라는 툴을 썼는데, 이름 그대로 시스템 핫 리로드를 1초 이하로 가능하게 해줘서 UI 프로토타이핑에 큰 도움이 되었음. https://github.com/TheBevyFlock/bevy_simple_subsecond_system
    • zig 팀도 LLVM 없이 자체 컴파일러(백엔드)를 만들어 빌드 타임을 매우 빠르게 하는 시도를 하는 걸로 앎
    • 예전에는 Cranelift가 macOS aarch64 지원 안 했던 걸로 아는데, 최근엔 지원한다는 사실 알게 됨
    • 16초 빌드 타임 때문에 러스트 포기할 뻔했다는 건 좀 과한 거 아님?
  • 러스트가 느리다고 생각하지 않음. 동등 수준 언어 대비 충분히 빠르고, 15분씩 걸리던 C++/Scala 컴파일에 비하면 훨씬 빠름

    • 나도 공감. 러스트 빌드가 딱히 불편함 느껴본 적 없음. 아마 초창기 나쁜 인식이 계속 이어져 이런 평이 생긴 듯
    • 컴파일 시 메모리 사용량이 C/C++에 비해 매우 큼. 내가 유튜브 데모용 VM에서 러스트 대형 프로젝트 컴파일하려면 8GB 이상 필요함. C/C++에선 이런 걱정 안 함
    • C++ 템플릿이 튜링 완전하다는 점에서, 실제 코드 스타일 고려 안 한 채 빌드 타임만 비교하는 건 의미 없음
  • 예전 C++ 개발자 입장에서 러스트 빌드가 느리다는 주장 잘 이해 안 됨

    • 그래서 러스트가 C++ 개발자를 타깃으로 한다고 평가되는 것임. C++ 경험이 많은 개발자는 이미 도구의 불편함을 잘 견디는 Stockholm syndrome이 있음
    • C++보다 빨라도 절대치로 느릴 순 있음. C++ 빌드의 악명은 이미 모두가 잘 알듯 극악임. 러스트는 구조적 언어 문제를 안고 있진 않아서 기대치가 더 높아지는 듯
    • 새로운 기능은 계속 추가되는데, 실제 사용자 의견 듣고 문제 해결은 잘 안 하는 classic 사례라는 생각임
    • C의 컴파일 단계가 적고 단순해 빨랐지만, C++는 템플릿 사용으로 인해 오히려 대부분의 encapsulation work들을 무너뜨렸다고 느껴짐. 하나의 템플릿 헤더만 바꿔도 결국 전체 프로젝트의 98%가 영향을 받는 기분
  • 인크리멘탈 컴파일이 정말 강력함. 초기 빌드 후 인크리멘탈 캐시 스냅샷을 굳혀서 변화가 없으면 그대로 빠르게 빌드/배포 활용 가능함. docker와 궁합도 좋음. 컴파일러 버전이나 큰 웹사이트 업데이트 제외하곤 이미지 빌드 레이어를 안 건드림. 오직 코드 변화만 있을 땐 해당 레이어가 재빌드 안 되게 설정하면 효율적임

    • 내 프로젝트 인크리멘털 아티팩트가 150GB를 넘음. docker 이미지를 이 정도로 커서 썼을 때 실제로 아주 큰 문제들이 생겼었음
  • 내 홈페이지 빌드 타임은 73ms임. static site generator는 17ms만에 다시 컴파일. 실제 generator 실행은 56ms밖에 안 걸림. zig 빌드로그 출력 첨부함

    • C/C++에는 러스트 좋다는 댓글, 러스트엔 Zig 좋다는 댓글이 항상 붙는 것 같음. (알고보니 이 댓글의 작성자가 zig 메인 개발자) 언어 전도는 커뮤니티에 해로우며, 실제로는 반감만 불러오지 새로운 사용자를 유입시키지 않는다고 생각함. 진정 언어를 사랑한다면 이런 전도 문화를 억제하는 게 도움 됨
    • 단일 컴파일 타임 지표 제시 말고, 원글 주제와 직접적인 논의나 해석이 있었으면 더 좋았을 것 같음
    • 내 러스트 웹사이트(react-like 프레임워크 및 실질적 웹서버 포함)도 cargo watch로 인크리멘탈 빌드 시 약 1.25초 소요됨. subsecond[0] 같이 incremental linking, hotpatch까지 쓰면 더 빨라짐. Zig만큼은 아니지만 거의 비슷함. 만약 위에서 이야기한 331ms가 clean(캐시 없이) 빌드라면, 내 웹사이트 clean 빌드 12초보다는 훨씬 빠름. [0]: https://news.ycombinator.com/item?id=44369642
    • @AndyKelley에게 꼭 묻고 싶은데, zig가 컴파일링이 엄청 빠르고 러스트·스위프트가 늘 느린 결정적 이유가 뭐라고 생각하는지 궁금함
    • Zig는 메모리 안전성 보장하지 않는 거 맞지?