1P by GN⁺ | ★ favorite | 댓글 1개
  • Docker 배포용 Rust 웹사이트 빌드에서 의존성을 캐시해도 최종 크레이트만 약 175초 걸리며, 병목이 rustc 내부와 LLVM 최적화 단계로 좁혀짐
  • cargo-chef, cargo --timings, -Zself-profile, measureme를 차례로 적용한 결과, 단순 의존성 문제가 아니라 LTO와 LLVM 코드 생성 비용이 빌드 시간을 지배함
  • 오래된 Cargo.toml 설정의 lto = "thin"debug = "full"이 큰 영향을 줬고, 둘을 끄자 최종 바이너리 빌드는 172.2초에서 약 50초 수준으로 줄어듦
  • LLVM 추적에서는 OptFunction, InlinerPass, core::ptr::drop_in_place, 큰 async 함수와 제네릭 단형화가 주요 비용으로 나타났고, 인라이닝 축소·함수 분리·Pin<Box<dyn Future>>·제네릭 제거가 추가 개선을 만듦
  • 마지막으로 -Zshare-generics와 Debian 기반 빌드 전환을 적용하자 컴파일 시간이 29.1초에서 9.1초까지 떨어져, 코드 구조뿐 아니라 allocator와 musl 타깃 여부도 빌드 시간에 크게 작용함

Docker 빌드에서 드러난 병목

  • 웹사이트는 주로 단일 Rust 바이너리로 제공되며, 기존에는 정적 링크 바이너리를 빌드해 서버로 복사한 뒤 서비스를 재시작했음
  • 컨테이너 기반 배포로 옮기려 하자 Docker에서 빠른 Rust 빌드를 구성하는 일이 예상보다 까다로웠음
  • 기본 Dockerfile은 소스가 바뀔 때마다 모든 것을 다시 빌드함
    • rust:1.87-alpine3.22를 builder로 쓰고 x86_64-unknown-linux-musl 타깃으로 빌드
    • 최종 이미지는 Alpine에 바이너리만 복사
    • 이 방식의 클린 빌드는 crates 다운로드 10초를 포함해 3분 51초 걸림

cargo-chef로 의존성 캐시를 분리했지만 부족했음

  • cargo-chef는 워크스페이스에서 단순화된 recipe 파일을 만들고, 이를 기반으로 의존성을 별도 Docker 캐시 레이어에 미리 빌드함
  • 웹사이트가 수백 개 의존성을 사용하므로 캐시 효과가 클 것으로 기대했음
  • 실제 측정에서는 의존성 빌드가 1분 7초, 캐시된 의존성을 사용한 최종 바이너리 빌드가 2분 50초였음
  • 전체 시간의 약 25%만 의존성에 쓰였고, 나머지 대부분은 web-http-server 최종 크레이트의 단일 rustc 호출에서 소비됨

cargo --timingsrustc self-profile

  • cargo build --release --timings는 크레이트별 컴파일 시간을 보여주며, 최종 크레이트 시간은 174.1초cargo build 출력의 2분 54초와 대략 일치함
  • 병목이 최종 크레이트 하나에 집중되어 있어 cargo --timings만으로는 세부 원인을 파악하기 어려웠음
  • rustc의 self-profile 기능을 쓰기 위해 -Zself-profile을 사용함
    • 안정 컴파일러에서 불안정 -Z 플래그를 쓰기 위해 RUSTC_BOOTSTRAP=1을 사용
    • cargo-chef 캐시 무효화를 피하려고 cargo rustc -- -Z self-profile 대신 RUSTFLAGS='-Zself-profile'을 사용
  • measuremesummarize, flamegraph, crox 도구로 self-profile 데이터를 분석함
  • summarize 상위 항목은 LLVM 관련 작업에 몰려 있었음
    • LLVM_lto_optimize: 851.95초, 전체의 33.389%
    • LLVM_module_codegen_emit_obj: 674.94초, 26.452%
    • LLVM_thin_lto_import: 317.75초, 12.453%
    • LLVM_module_optimize: 189.00초, 7.407%
  • flamegraph에서는 codegen_module_perform_lto가 전체 시간의 약 80% 를 차지함

LTO와 디버그 심볼 설정의 영향

  • Rust 컴파일러는 크레이트를 codegen unit으로 나누고 LLVM에 별도 모듈로 넘김
  • LTO는 링크 시점에 codegen unit 또는 크레이트 사이에서 인라이닝과 최적화를 수행하는 옵션임
  • Cargo와 rustc의 LTO 선택지는 다음과 같음
    • LTO 끔
    • "thin" LTO
    • "fat" LTO
    • 명시하지 않으면 단일 크레이트 안에서만 제한되는 “thin local LTO”
  • 기존 Cargo.toml에는 몇 년 전 설정한 값이 남아 있었음
    • lto = "thin"
    • debug = "full"
  • debug = "full"은 release 프로파일에서 기본적으로 제외되는 전체 디버그 심볼을 활성화함
  • 다양한 ltodebug 조합을 측정한 결과는 차이가 컸음
    • LTO 끔, debug=none: 50.0초 / 21.0MiB
    • Thin local LTO, debug=full: 88.2초 / 256.8MiB
    • "thin" LTO, debug=full: 172.2초 / 197.5MiB
    • "fat" LTO, debug=full: 287.1초 / 155.9MiB
  • 전체 디버그 심볼은 컴파일 시간을 30~50% 늘렸고, fat LTO는 LTO를 완전히 끈 경우보다 약 4배 오래 걸림
  • LTO와 디버그 심볼을 꺼도 최종 바이너리 하나를 컴파일하는 데 여전히 약 50초가 남음

증분 컴파일 대신 Docker 캐시를 유지한 이유

  • 로컬 개발에서는 /target 디렉터리를 Dockerfile의 cache mount로 연결하고 빌드 사이에 유지하면 증분 컴파일을 사용할 수 있음
  • 다만 docker build가 매번 깨끗한 환경을 가질 수 있다는 점을 유지하고, Docker 자체 캐시 시스템을 활용하기 위해 cargo-chef를 계속 사용함

LTO 이후에도 남은 LLVM 최적화 비용

  • LTO와 디버그 심볼을 끈 뒤에도 최종 바이너리 컴파일은 약 50초 걸렸음
  • self-profile을 다시 보면 시간의 약 70%LLVM_module_optimize에 쓰였고, 이는 LLVM이 코드를 최적화하는 단계임
  • release 프로파일의 기본 opt-level = 3을 낮춰 최종 바이너리만 덜 최적화하는 설정을 실험함
    • 의존성은 캐시되므로 profile.release.package."*"에서 opt-level = 3 유지
    • 최종 크레이트만 opt-level을 낮춤
  • 측정 결과는 최적화 유무에 따라 크게 갈림
    • 최종 opt-level=0: 약 15초
    • 최종 opt-level=1: 약 48초
    • 최종 opt-level=2 또는 3: 약 50~55초
    • 최종 opt-level="z": 약 42초
  • 최종 바이너리에 어떤 최적화든 켜면 약 50초 기준선이 생기고, 최적화를 완전히 끄면 약 15초로 빨라짐

LLVM 추적 데이터 수집의 어려움

  • rustc에는 LLVM 정보를 보기 위한 플래그가 있음
    • -Z time-llvm-passes: LLVM 프로파일 정보를 일반 텍스트로 출력
    • -Z llvm-time-trace: Chrome tracing format으로 LLVM 프로파일 출력
  • -Z time-llvm-passes는 Docker BuildKit의 기본 로그 제한에 걸렸음
    • BUILDKIT_STEP_LOG_MAX_SIZE
    • BUILDKIT_STEP_LOG_MAX_SPEED
  • 이 환경 변수는 docker build 호출이 아니라 Docker daemon에 설정해야 하며, Linux에서는 systemd drop-in으로 docker.service에 설정할 수 있음
  • 제한을 풀면 약 20만 줄의 텍스트가 출력되어 직접 다루기 어려웠음
  • -Z llvm-time-trace*.llvm_timings.json 파일을 생성했지만, 최종 바이너리 추적 파일은 1.4GiB의 단일 줄 JSON이었음
  • Firefox Profiler, Perfetto UI, Chromium의 chrome://tracing 모두 이 파일을 다루는 데 문제가 있었음
  • JSON을 JSONL로 바꿔 일반 도구로 처리함
    • 단일 JSON 객체의 traceEvents 배열을 이벤트별 한 줄로 분리
    • 변환 뒤 이벤트 수는 7,301,865줄이었음

LLVM 이벤트에서 보인 병목

  • LLVM 추적 이벤트는 주로 "ph":"X"인 complete event였고, dur 필드가 마이크로초 단위 duration을 나타냄
  • "ph":"M"은 metadata event였으며, 이 분석에서는 유용한 정보가 많지 않았음
  • aggregate 이벤트에서 시간이 많이 쓰인 항목은 다음과 같음
    • Total ModuleInlinerWrapperPass: 665.37초
    • Total ModuleToPostOrderCGSCCPassAdaptor: 656.47초
    • Total DevirtSCCRepeatedPass: 632.44초
    • Total OptFunction: 189.62초
    • Total InlinerPass: 182.25초
  • 이 실행은 16코어 머신에서 약 110초 걸렸기 때문에 일부 pass 시간은 중복 집계됨
  • 큰 축은 함수 최적화인 OptFunction 과 인라이닝인 InlinerPass 였음

인라이닝 임계값 조정

  • LLVM 인라이닝 옵션은 rustc-C llvm-args로 전달할 수 있음
  • 2025년 6월 기준 rustc -C llvm-args='--help-list-hidden'에는 인라이닝 관련 옵션이 약 100개 있음
  • 실험에 사용한 옵션은 세 가지였음
    • --inlinedefault-threshold=225
    • --inline-threshold=225
    • --inlinehint-threshold=325
  • threshold는 대략 비용이 그 값보다 낮은 함수의 인라이닝을 허용하므로, 값을 낮추면 인라이닝이 줄어듦
  • 세 임계값을 50으로 낮추자 시간이 48.8초에서 42.2초로 감소함
  • 부하가 거의 없는 개인 웹사이트라는 사용 사례에서는 threshold 10도 유망한 선택지로 봄

OptFunction과 제네릭 단형화

  • OptFunction 이벤트의 args.detail에는 최적화 중인 함수의 mangled symbol이 들어 있음
  • rustfilt로 demangle하면 원래 Rust 심볼을 볼 수 있음
    • __rustc::__rust_alloc
    • serde_json::value::to_value
  • 같은 serde_json::value::to_value가 여러 해시로 나타난 이유는 제네릭 함수가 서로 다른 타입 파라미터로 단형화되기 때문임
  • 다른 크레이트의 함수도 최종 크레이트에서 최적화됨. 함수가 특정 타입으로 단형화되는 위치가 호출하는 크레이트의 문맥이기 때문임
  • 최적화 시간이 많이 든 함수 예시는 다음과 같음
    • web_http_server::photos::PhotosState::new 안의 closure
    • web_http_server::run 안의 closure
    • tokio_postgres::connect_raw
    • pulldown_cmark의 500줄 규모 제네릭 함수
    • core::ptr::drop_in_place의 여러 구체 타입
  • 바깥쪽 크레이트 이름으로 대략 집계하면 core61.53초로 가장 컸고, 그중 84%는 core::ptr::drop_in_place의 파라미터화였음

v0 심볼 맹글링으로 async 함수 위치를 더 명확히 봄

  • 기본 legacy symbol mangling은 closure를 구분하기 어려웠음
  • -C symbol-mangling-version=v0를 추가하면 closure 번호와 제네릭 타입 정보가 더 잘 드러남
  • 예를 들어 serde_json::value::to_value가 어떤 web_http_server 타입으로 단형화되었는지 전체 제네릭 인자를 볼 수 있었음
  • v0 출력에서 비싼 항목은 다음과 같았음
    • <web_http_server::photos::PhotosState>::new::{closure#0}: 1.99초
    • web_http_server::run::{closure#0}: 1.56초
    • core::ptr::drop_in_place::<axum::routing::Endpoint<web_http_server::AppState>>: 1.22초
  • 겉보기에는 작은 closure였지만, LLVM IR을 덤프해 보니 async 함수와 async block이 내부적으로 nested closure로 표현되어 있었음
  • Rust에는 이미 async function/block의 맹글링 관련 open issue가 있었음

큰 async 함수와 Pin<Box<dyn Future>>

  • 비싼 항목은 closure 자체라기보다 큰 async 함수 본문이었음
  • PhotosState::new 관련 최적화 시간은 처음에 총 5.3초였음
  • 단순히 함수를 쪼갠 첫 시도는 4.66초로 조금만 줄었음
  • 인접한 .await를 묶어 .await 수를 10개에서 3개로 줄인 시도는 오히려 6.24초로 늘어남
  • async 함수는 내부적으로 복잡한 state machine으로 낮아지므로, caller에서 구현 세부를 감추기 위해 Future를 trait object로 지우는 방식을 시도함
  • 사용한 함수는 impl Future<Output = T>Pin<Box<dyn Send + Future<Output = T>>>로 감싸는 형태였음
  • .await 지점마다 erase(get_img_candidates()).await?처럼 적용한 결과:
    • PhotosState::new 관련 시간은 2.14초로 감소
    • 전체 빌드 시간은 프로파일링 없이 48.8초에서 46.8초로 감소
  • #[inline(never)]와 poll 함수 인라이닝 비활성화도 시도했지만, boxing만큼 좋지는 않았음

여러 변경을 합친 결과

  • 적용한 접근은 세 가지였음
    • LLVM args로 인라이닝 축소
    • 메인 크레이트의 비싼 함수 분리와 async Future boxing
    • 의존성 API의 제네릭을 줄여 최종 크레이트에서 다시 컴파일되는 부분 감소
  • 최종 Dockerfile에는 세 인라이닝 threshold를 10으로 낮추는 RUSTFLAGScargo chef cookcargo build 양쪽에 적용함
  • 메인 크레이트에는 10개 파일에 걸쳐 898줄 추가, 657줄 삭제가 생김
  • 의존성 쪽 변경도 함께 들어감
    • pulldown-cmark의 제네릭 함수 비제네릭화 PR
    • lol_html, deadpool_postgres에서 사용하는 API의 비제네릭 버전을 노출하는 로컬 크레이트
  • 이 조합으로 최종 컴파일 시간은 32.3초가 됨

2025-06-27 업데이트: -Zshare-generics와 Alpine 제거

  • Bluesky와 Lobsters에서 받은 제안으로 두 가지를 추가 실험함
    • -Zshare-generics 활성화
    • Alpine에서 벗어나기
  • -Zshare-generics는 크레이트 의존성의 제네릭 인스턴스를 재사용하는 플래그임
    • release 빌드에서는 기본 활성화되지 않음
    • stable toolchain의 dev build에서는 활성화되어 있음
    • 이 플래그는 nightly에서만 사용 가능함
  • -Zshare-generics를 켠 결과 전체 컴파일 시간은 32.3초에서 29.1초로 줄었음
  • drop_in_place 인스턴스는 여전히 많이 컴파일되었지만, 해당 최적화 시간은 21.7초에서 17.4초로 감소함
  • Alpine에서 Debian으로 바꾸고 --target=x86_64-unknown-linux-musl을 제거하자 전체 컴파일 시간은 29.1초에서 9.1초로 크게 줄었음
  • 제안의 배경에는 기본 allocator가 빌드 시간에 큰 영향을 줄 수 있다는 점이 있었음

최종 수치와 남은 과제

  • 최종 변화는 다음과 같음
    • 시작점: 약 175초
    • LTO와 디버그 심볼 비활성화: 51초, -71%
    • 최종 크레이트 opt-level = 1: 48.8초, -4%
    • -C llvm-args로 인라이닝 축소: 40.7초, -16%
    • 로컬 코드 변경: 37.7초, -7%
    • 의존성 변경: 32.3초, -14%
    • -Zshare-generics: 29.1초, -10%
    • Alpine 제거: 9.1초, -69%
  • 분석 과정에서 도구와 문서는 실제 개선을 만들 만큼 잘 동작했음
  • 다만 몇 가지 복잡한 문제는 남아 있음
    • 깊은 async 함수 호출 그래프의 컴파일 시간은 더 개선될 필요가 있음
    • core::ptr::drop_in_place<T>T를 정의한 크레이트에서 컴파일하도록 특수 처리하는 방안은 일부 경우에 도움이 될 수 있으나, 제네릭 타입에는 적용하기 어렵고 사용되지 않는 drop glue까지 컴파일할 위험이 있음
    • -Zshare-generics는 도움이 되지만 완전한 해결책은 아님
    • 코드베이스의 어느 부분이 컴파일 시간을 많이 쓰는지 격리하고 완화책을 제안하는 도구가 더 필요할 수 있음
  • 실용적으로는 최종 크레이트에 opt-level = 0을 설정하는 선택도 충분할 수 있음

댓글과 토론

Hacker News 의견들
  • Epic용 RAD 디버거를 만드는 Ryan Fleury는 27.8만 줄짜리 C 코드를 유니티 빌드로 구성했고, 괜찮은 Windows 머신에서 클린 컴파일이 1.5초 걸림
    컴파일이 엄청나게 빠를 수 있다는 명확한 사례로 보이고, Rust나 Swift 같은 언어도 비슷한 방식으로 비슷한 속도를 낼 수 없는 이유가 궁금함

    • 컴파일러가 빌드 시점에 더 많은 일을 해줄수록 빌드는 오래 걸림
      Go가 거대한 코드베이스에서도 1초 미만 빌드를 하는 이유는 빌드 시점에 많은 일을 하지 않기 때문임. 모듈 시스템과 타입 시스템이 단순하고, 많은 처리를 런타임의 가비지 컬렉션에 맡김
      매크로, 고급 타입 시스템, 빌드 시점의 강한 견고성 보장을 원하면 그 비용을 내야 함
    • Rust와 Swift는 C 컴파일러보다 훨씬 많은 일을 함
      빌림 검사기에 필요한 분석도 공짜가 아니고, 두 언어의 여러 컴파일 시점 검사도 마찬가지임. C는 기본 문법 외에는 사실상 컴파일 시점 검사를 거의 하지 않아서 foo(char)foo(int) 같은 위험한 호출도 가능함
    • C 코드가 빠르게 컴파일될 수 있다는 사실 자체는 별로 흥미롭지 않음. Go도 빠른 컴파일을 목표로 설계된 언어임
      컴파일 자체의 본질적 문제가 아니라, 어려운 문제는 Rust의 의미론을 빠르게 컴파일하는 것이고, Rust 웹사이트 FAQ에도 있는 주제임
    • 2000년대에 수만 줄 C++ 프로젝트가 낡은 컴퓨터에서도 1초도 안 되어 컴파일되는 걸 본 적 있음. 반면 Boost를 쓴 Hello World는 몇 초가 걸렸음
      언어만의 문제가 아니라 코드 구조와 컴파일 비용이 큰 기능 사용의 문제임. C 매크로로 Doom을 쓰면 빠르지 않을 수 있고, Rust도 빠르게 컴파일되도록 작성할 수 있다고 봄
    • 그 27.8만 줄을 단순한 Rust로, 제네릭이나 매크로 없이, 단일 크레이트와 의존성 없이 다시 쓰면 비슷한 컴파일 시간을 낼 수 있을 것 같음
      Rust 컴파일러는 코드가 단순하면 매우 빠를 수 있음. 느려지는 건 의존성, 무거운 추상화, 매크로, 제네릭, 트레이트, 깊은 의존성 트리 때문임
  • Go가 반대로 최적화보다 컴파일 속도를 택해서 좋음
    서버, 네트워킹, 접착 코드를 쓰는 작업에서는 빠른 컴파일이 절대적으로 중요함. 어느 정도의 타입 안전성은 원하지만, 대충 프로토타입도 못 하게 막는 과한 타입 시스템은 원하지 않고, 가비지 컬렉션도 도움이 됨
    Google의 경험상 소프트웨어 개발을 확장하려면 단순한 타입 시스템, 가비지 컬렉션, 매우 빠른 컴파일이 순수 런타임 처리량이나 의미론적 정확성보다 중요하다는 결론에 이른 것 같음. Go로 작성된 네트워킹과 대규모 인프라 소프트웨어를 보면 그 선택이 잘 맞아떨어졌다고 봄
    물론 가비지 컬렉션을 용납할 수 없거나 정확성이 개발 속도보다 중요한 영역도 있지만, 그런 일을 하지 않아서 Go의 절충에 만족함

    • Go를 좋아하지만, 이게 Google의 엄청난 집단 지혜와 경험의 산물이라고 보지는 않음
      정말 그랬다면 널 포인터 예외를 정적으로 제거하는 일이 가치 있다고 판단했을 것임. 그냥 Google에 있던 몇 사람이 자신들이 원하는 방식으로 언어를 만든 결과에 가깝다고 봄
    • 빠른 컴파일, 적당한 타입 안전성, 가비지 컬렉션이라는 설계 공간은 이미 Java가 차지하고 있었고 Java도 빌드가 매우 빠름
      Go는 설계자들이 새 프로그래밍 언어를 만들고 싶어서 생긴 것처럼 보임. 구현상 좋은 점은 있지만, 원래 목표였던 C/C++/Java 개발자보다 Python/Ruby/JS 쪽 사용자들을 더 많이 끌어왔음
      스크립트 언어 사용자들은 너무 고급은 아니지만 타입 시스템이 있고, 빠른 반복 주기라는 스크립트 느낌을 유지하는 언어를 원했음. Java는 낡고 멋없어 보였고, 라이브러리나 설정, 콘퍼런스 같은 지적 공간도 이미 포화 상태였음
    • 이야기로는 Google 개발자 몇 명이 C++ 프로젝트 컴파일을 기다리다가 Go를 설계했다고 함
    • “거슬리는 타입”이 뭔지 모르겠음. 타입은 데이터를 올바르게 표현하거나 그렇지 않거나 둘 중 하나임
      Haskell, Idris, PureScript를 포함한 어떤 언어에서도 컴파일러를 조용히 시키도록 타입을 억지로 만들 수는 있음
    • 그게 바로 Go가 목표로 한 영역이고, 작업에 맞는 도구를 고르는 것보다 나은 건 없음
      다만 본 적 있는 유일한 큰 함정은 채널을 통한 변경 가능한 공유 상태 병렬 처리가 미묘하고 악용 가능하게 틀릴 수 있다는 점임. 대부분은 채널을 그렇게 쓰지 않는 것 같음
      Rust를 쓰는 이유는 내 일이 그런 종류가 아니기 때문임. 보통 느린 알고리즘을 더 느린 하드웨어에 욱여넣어야 하고, 문제들은 대개 거의 병렬화하기 쉽지만 딱 그렇지는 않음
  • Rust 프로젝트는 종종 겉보기보다 훨씬 커지는 점이 흥미로움
    첫째, 의존성은 체감 코드 크기로 잘 환산되지 않음. C++에서는 큰 프로젝트 의존성을 벤더링하거나 아예 쓰지 않는 경우도 많아서, 40만 줄 코드베이스를 보고 “느리지만 코드가 많으니 그럴 만하다”고 느끼기 쉬움
    둘째, 더 나쁜 문제는 매크로임. 수십~수백 줄로 확장되는 매크로는 1만 줄 프로젝트를 순식간에 백만 줄 괴물로 만들 수 있음
    셋째, 제네릭도 같은 문제가 있음. 제네릭 인스턴스화마다 CPU를 먹음
    다만 Rust를 변호하자면, 이 기능들은 훌륭함. C로 10만 줄, C++로 2.5만 줄이었을 코드를 Rust 몇천 줄로 줄여줌
    하지만 생태계에서 과용되는 면이 있어 느리게 느껴짐. 직장에서 쓰는 async-graphql은 라이브러리 자체는 좋지만 프로시저 매크로를 엄청나게 많이 쓰고, 저장소에는 성능 이슈가 몇 년째 열려 있음. 타입을 하나 추가할 때마다 컴파일러가 느려지는 게 체감됨

    • 타입을 하나 추가할 때마다 컴파일러가 느려지는 게 체감된다는 부분은 당연할 수 있음
      기억이 맞다면 컴파일러 성능은 타입의 깊이에 대해 지수적으로 나빠지고, GraphQL은 중첩 타입을 정말 좋아함
    • 매크로가 얼마나 많은 코드를 생성하는지 분석하는 지원이 최근 추가됨
      https://nnethercote.github.io/2025/06/26/how-much-code-does-...
    • “C 10만 줄이나 C++ 2.5만 줄이었을 코드를 Rust 몇천 줄로 줄인다”는 이야기는 더 작은 프로그램에서는 어떻게 되는지 궁금함
      여러 작성자가 작은 C 유틸리티를 Rust로 다시 쓰거나 비슷한 것을 Rust로 만듦. 10만 줄 C 프로그램을 Rust로 재작성한 사례도 많겠지만, HN이나 GitHub 등에서 계속 보이는 건 훨씬 작은 프로그램들임
      작은 프로그램에서 Rust 컴파일 시간이 C와 어떻게 비교되는지 알고 싶음. 프로그램 크기가 아니라 컴파일 속도를 묻는 것임
      리소스 사용량도 궁금함. 예전에 측정했을 때 Rust 컴파일러 툴체인은 내가 쓰는 GCC 툴체인보다 대략 2배 컸음
      이런 작은 프로그램들은 크기상 어떤 언어로 쓰였든 미발견 메모리 안전성 문제가 있을 가능성이 낮아 보임. 10만 줄 C 프로그램과 달리 비교적 감사하기 쉬움
  • 정적으로 링크된 단일 바이너리를 설치하는 게 컨테이너를 관리하는 것보다 명백히 단순한데, 그 사람은 혼동한 것 같음

    • Docker가 정확히 뭘 하는지 완전히 이해하지 못한 느낌도 듦
      “변경이 있을 때마다 모든 것을 처음부터 다시 빌드한다”는 식으로 Docker 이미지 안에서 전부 빌드하는 걸 말했는데, 혼자 빌드하고 CI/CD도 필요 없다면 로컬 편의 기능을 다 써서 빌드한 뒤 결과물만 Docker 컨테이너에 넣어도 문제 없음
      경로 같은 설정이 실수로 들어가는지만 확인하면 됨. 내 경우라면 내 사용자명이 빌드했고 src 디렉터리가 있다는 정도만 드러날 뿐이라 별로 걱정하지 않음
      전문적인 환경의 CI/CD에서는 최소 환경에서 프로젝트를 부트스트랩할 수 있음을 보장하는 게 좋지만, 개인 프로젝트에는 그 정도까지 필요 없음
    • 글에서 목표는 단순화가 아니라 현대화였음
      컨테이너는 프로세스 격리, 보안 향상, 표준화된 로그, 성숙한 수평 확장성 같은 여러 이점을 제공함
  • Cranelift가 어디에 언급되어 있는지 모르겠음
    게임 개발에서 Rust를 거의 버리려 했던 이유가 컴파일 시간인데, 파고들어 보니 LLVM이 최적화 수준과 관계없이 매우 느렸음. Jai 개발자들이 해온 말도 그 점임
    그래서 원글 작성자에게 Cranelift가 관련 있을 수 있음. 내 게임 빌드를 16초에서 4초로 줄여줬고, Cranelift 팀의 작업은 대단함

    • gcc -O0는 C와 C++ 모두에서 clang -O0보다 더 느리지 않나?
    • 최근 Bevy 게임 잼에 참여했는데, 커뮤니티에 Dioxus에서 나온 subsecond라는 새 도구가 있음
      이름처럼 시스템의 1초 미만 핫 리로딩을 제공해서 프로토타이핑이 아주 쾌적했음. 특히 UI를 반복해서 다듬을 때 좋았음
      https://github.com/TheBevyFlock/bevy_simple_subsecond_system
    • Zig 팀도 매우 빠른 빌드 시간을 위해 같은 방향, 즉 LLVM 제거를 하고 있는 것 같음
    • 예전에 확인했을 때는 macOS aarch64 지원이 없었는데, 이제는 지원되는 것 같음
    • 빌드 시간이 16초라서 Rust를 버리려 했다는 게 놀라움
  • 전혀 느리다고 느끼지 않음. 이 정도 복잡도의 다른 언어와 비슷한 성능으로 보이고, 같은 범주에 둘 만한 C++와 Scala의 15분 빌드보다 훨씬 빠름

    • 나도 잘 이해가 안 됨. 작업 중에 Rust 컴파일러 때문에 거의 불편하지 않음
      초기에 매우 나빴던 시절의 인식이 계속 남아 있는 것 같음
    • 컴파일할 때 메모리 사용량은 C/C++에 비해 꽤 큼
      YouTube 채널 데모용으로 가상 머신을 쓰는데, Rust로 큰 것을 컴파일하려면 8GB 이상이 필요함. C/C++에서는 그런 걱정을 해본 적이 없음
    • C++ 템플릿이 튜링 완전이라면 실제 코드를 고려하지 않고 컴파일 시간을 불평하는 건 별 의미가 없음
  • 예전 C++ 개발자 입장에서는 Rust 컴파일이 느리다는 말이 잘 이해되지 않음

    • Rust가 C++ 개발자를 겨냥한다고 여겨지는 이유 중 하나임
      C++ 개발자들은 이미 그런 도구 체인을 견딜 수 있는 스톡홀름 증후군을 갖고 있음
    • C++만큼 느리지 않더라도 절대적으로 느릴 수는 있음
      C++ 컴파일 문제가 왜 생기는지는 매우 잘 이해되고 문서화되어 있고, 컴파일 시간 면에서 지구상 최악의 언어 중 하나임. Rust는 그런 언어 수준의 문제를 공유하지 않으니 기대치가 더 높은 건 당연함
    • C가 컴파일과 링크 단계를 줄이며 캡슐화 쪽으로 쌓아온 작업이 마음에 들었는데, C++가 모든 것에 템플릿을 요구하는 단순한 방식으로 거의 다 되돌려버림
      헤더의 템플릿 하나를 바꾸면 코드의 98%가 영향을 받는 식임
    • 전통적으로 내 C++는 기본 상태의 Rust보다 항상 빨랐음
      미리 컴파일된 헤더, 증분 컴파일러, 증분 링커, 의존성의 바이너리 라이브러리 사용 덕분이었고, 요즘은 모듈도 있음
      Rust는 처음부터 컴파일하고, 의존성 그래프의 충돌 설정 때문에 일부 크레이트가 여러 번 컴파일될 수 있음. 바이너리 라이브러리는 scache 같은 것에 추가 설정이 필요함
    • 전형적인 패턴임: 새 기능은 좋다고 하면서, 사용자와 대화하고 실제 문제를 고치는 건 안 함
  • 증분 컴파일은 좋음
    원하면 최초 클린 빌드 뒤 증분 캐시를 고정해서 업데이트 빌드와 배포에 쓰면, 중간 상태가 점진적으로 캐시를 손상시킬 위험을 줄일 수 있음
    Docker와도 잘 맞음. 새 컴파일러 버전이나 큰 웹사이트 업데이트 때는 증분 캐시가 있는 레이어를 다시 빌드하고, 그 외에는 스냅샷에서 실행해 최신 웹사이트 업데이트 상태를 빌드한 뒤 나온 정적 바이너리를 업로드·배포하면 됨
    단순 코드 변경만으로 클린 빌드의 증분 컴파일 캐시를 저장한 레이어가 다시 빌드되지 않게 설정하면 됨

    • 내 프로젝트의 중간 산출물만 150GB 이상
      예전에 그 정도로 큰 Docker 이미지를 다뤘을 때 엄청난 문제가 있었음
  • 내 홈페이지는 다시 빌드하는 데 73ms 걸림. 정적 사이트 생성기를 다시 컴파일하는 데 17ms, 실행하는 데 56ms임

    andy@bark ~/d/andrewkelley.me (master)> zig build --watch -fincremental  
    Build Summary: 3/3 steps succeeded  
    install success  
    └─ run exe compile success 57ms MaxRSS:3M  
    └─ compile exe compile Debug native success 331ms  
    Build Summary: 3/3 steps succeeded  
    install success  
    └─ run exe compile success 56ms MaxRSS:3M  
    └─ compile exe compile Debug native success 17ms  
    watching 75 directories, 1 processes  
    
    • C/C++ 글마다 Rust가 좋다는 댓글이 달리듯, Rust 글마다 Zig가 좋다는 댓글이 달림
      알고 보니 Zig의 주요 작성자에게 답한 셈이지만, 언어 전도는 Rust의 최악의 부분이고 Rust로 사람을 바꾸기보다 반Rust 정서를 더 키웠을 가능성이 큼. 진심으로 자기 언어를 아낀다면 전도를 받아들이기보다 커뮤니티를 그 방향에서 멀어지게 해야 함
    • 흥미롭긴 한데, 이 댓글은 게시된 글과 연결되거나 단일 컴파일 시간 수치 이상의 통찰이 있었으면 훨씬 나았을 것 같음
      여기서 뭘 받아들여야 하는지 모르겠음. Zig는 좋고 Rust는 나쁘다는 뜻인가?
    • 내 비정적 Rust 웹사이트는 실제 웹서버와 React 비슷한 템플릿 프레임워크를 포함하고, cargo watch로 증분 재컴파일하는 데 1.25초 걸림
      cargo watch는 외부 감시 도구라 프로세스를 죽이고 cargo run을 다시 실행함
      subsecond[0] 같은 것을 쓰면 훨씬 빨라질 수 있음. 증분 링크를 하고 실행 중인 바이너리를 핫패치함. Zig만큼 빠르진 않지만 꽤 가까움
      다만 위의 331ms 빌드가 클린 빌드라면, 내 웹사이트의 클린 빌드가 약 12초 걸리는 것보다 훨씬 빠름
      [0]: https://news.ycombinator.com/item?id=44369642
    • Zig처럼 컴파일이 매우 빠른 언어와 Rust나 Swift처럼 느린 언어를 가르는 핵심 요인이 궁금함
      핵심 차이가 무엇인지 알고 싶음
    • zig build--watch-fincremental이 추가된 줄 몰랐음
      주로 파일 변경 시 재컴파일용으로 watchexec -e zig zig build를 쓰고 있었음
  • M4 Pro Mac에서 Rust 대형 프로젝트 기준으로 삼는 Deno 전체 컴파일이 2분 걸림

    > cargo clean && time cargo build  
    
    cargo build 713.80s user 91.57s system 706% cpu 1:53.97 total  
    
    > cargo clean && time cargo build --release  
    
    cargo build --release 1619.53s user 142.65s system 354% cpu 8:17.05 total  
    

    증분 컴파일 없이 측정한 것이고, CI/CD 시스템이 있다면 릴리스 빌드를 지켜보고 있을 필요도 없음