Emitting metadata early makes building/checking Rust up to twice as fast
메타데이터를 일찍 내보내 Rust 빌드·검사를 최대 두 배 빠르게
Rust 프로토타입 Headstart는 크레이트의 함수 본문 검사가 끝나기 전에 인터페이스 메타데이터를 내보내 의존 크레이트의 작업을 앞당깁니다. 16코어에서 실제 프로젝트 13개의 빌드와 검사를 측정해 cargo check는 최대 54%, cargo build는 최대 42% 빨라졌으며, 개선 폭은 남는 코어 수와 의존 관계에 따라 달라집니다.
- 주제
AI 요약
Rust 프로젝트를 빌드할 때 의존 크레이트의 함수 본문까지 검사가 끝날 때까지 기다리지 않고, 먼저 확인된 인터페이스를 이용해 다음 크레이트의 작업을 시작하는 프로토타입 Headstart가 공개됐습니다. Rust 컴파일러 rustc와 빌드 도구 Cargo를 수정해 크레이트 간 작업을 더 일찍 겹칩니다. 저장된 결과를 재사용하는 캐시 방식이 아니라, 컴파일 순서를 앞당겨 유휴 코어를 활용하는 방식입니다.
인터페이스 메타데이터를 먼저 전달합니다
기존에는 의존 크레이트가 완전히 검사된 뒤에야 다음 크레이트가 작업을 시작합니다. 하지만 다음 크레이트가 타입 검사를 할 때 필요한 정보는 함수 본문 전체가 아니라 의존 크레이트의 인터페이스입니다. 예를 들어 함수의 인자와 반환 타입을 알면 호출부를 검사할 수 있습니다.
Headstart는 인터페이스 검사가 끝나면 .early-rmeta 파일을 먼저 씁니다. Cargo는 이 메타데이터를 받는 즉시 의존 크레이트의 컴파일을 시작합니다. 각 크레이트의 함수 본문 검사가 진행되는 동안 그 아래 단계의 크레이트도 작업을 이어갑니다.
cargo check에서는 의존 크레이트가 함수 본문 검사까지 마치기 전에 다음 크레이트가 일찍 전달된 메타데이터로 작업을 끝낼 수 있습니다. cargo build에서는 분석 작업을 먼저 진행한 뒤, 기계어 코드를 생성하기 전에 의존 크레이트의 완전한 메타데이터를 기다립니다. 대기 중인 컴파일은 작업 슬롯을 반납해 다른 작업이 사용할 수 있게 합니다.
오류 처리와 구현 범위
함수 본문에서 오류가 발견되면 빌드는 실패합니다. 오류 진단 내용과 종료 상태는 기존 동작과 같지만 진행 상황 출력과 JSON 메시지의 크레이트 간 순서는 달라질 수 있습니다. Cargo는 의존 크레이트가 모두 정상 종료된 뒤에야 해당 크레이트의 출력을 보고합니다. 의존 작업이 실패하면 그 출력은 버립니다.
이 방식에는 이미 진행한 하위 작업을 버릴 수 있고, 오류 보고가 조금 늦어지며, 한 번에 사용하는 메모리가 늘 수 있다는 비용이 있습니다. 설계 문서는 이런 위험과 초기 메타데이터에서 제외되는 정보를 별도로 설명합니다.
구현은 rustc 패치 6개와 Cargo 패치 3개로 구성됩니다. rustc에는 인터페이스와 함수 본문 분석을 나누는 질의가 추가됐고, 둘 사이에 .early-rmeta를 기록합니다. Cargo는 컴파일마다 -Zearly-metadata를 전달하고, 조기 메타데이터 알림을 받으면 의존 작업을 시작합니다. 전체 메타데이터가 준비될 때까지 멈춘 작업은 작업 슬롯을 돌려줍니다. 패치별 커밋 메시지와 테스트를 제공하며, 유지보수자에게 제안할 수 있도록 구성했습니다.
측정 결과
기본 rustc 프런트엔드로 실제 프로젝트 13개의 깨끗한 빌드를 측정했습니다. rust-analyzer, Zed, Bevy, Lemmy, Polars 등이 포함됩니다. cargo check는 최대 54%, cargo build는 최대 42% 빨라졌고, 측정한 프로젝트 중 느려진 경우는 없었습니다. 병렬 프런트엔드인 -Zthreads=8과 함께 사용하면 최대 25% 개선됐습니다. 이 수치는 16코어 시스템에서 얻었습니다.
개선은 빌드 과정에서 놀고 있던 코어를 활용하는 데서 나옵니다. 따라서 코어가 적거나 독립적인 의존 크레이트가 넓게 분산된 빌드에서는 이득이 줄어듭니다. 4코어 시스템에서는 rust-analyzer의 검사가 24%, 빌드가 13~15% 빨라졌고 codex-rs의 검사는 14% 빨라졌습니다. 폭이 넓은 빌드에서는 성능 차이가 없었습니다. codex-rs를 16코어에서 빌드한 사례에서는 워크스페이스 크레이트들이 순차적으로 컴파일되던 흐름이 겹치면서 전체 빌드가 37% 빨라졌습니다.
오류 진단과 종료 상태가 유지되는지 확인하는 테스트, 증분 변경 뒤 결과 비교, 조기 메타데이터에서 완전한 메타데이터로 바꿔 끼우는 테스트도 포함합니다. rustc-perf 컴파일 벤치마크 53개를 두 방식으로 실행해 빌드 성공 여부와 진단을 비교하는 절차도 제공합니다. 현재는 패치 묶음 형태의 프로토타입이며, 실제 채택 여부와 설계 논의는 별도 과제로 남아 있습니다.
Hacker News 반응
- @swiftcoder — 이 기능이 정식 컴파일러에 들어갈 방법이 있는지 기대하며 지켜보겠습니다.
- @knuckleheads — 지금 관련자들과 이야기하고 있습니다. 이 변경 사항 자체는 들어가지 않을 겁니다. LLM이 작성했고 더 넓은 설계를 거의 검토하지 않았기 때문입니다. 다만 비슷한 방식이 언젠가 등장하기를 바랍니다.
- @gchamonlive — 인코딩된 내용에 관한 논의가 도움이 된다면, 합성 코드가 실제 코드베이스에 들어가지 않아도 괜찮을 수 있다는 생각은 못 해봤습니다.
- @IshKebab — 쉽게 얻는 성능 개선처럼 보입니다. 아무도 먼저 하지 않았다는 게 조금 놀랍습니다. Rust의 AI 정책 때문에 누군가 직접 다시 구현해야 하겠지만, 좋은 아이디어임을 보여준 점은 좋습니다.
- @kibwen — Rust는 2019년에 메타데이터를 일찍 내보내는 파이프라이닝을 도입한 뒤 몇 년 동안 이 방향으로 나아갔습니다. 메타데이터를 간결하게 만들고 압축 방식을 시험하는 등 여러 작업도 있었습니다. 더 일찍 파이프라이닝하는 방안도 검토해 왔지만, 추측에 따라 먼저 승인한 크레이트에서 오류가 발견됐을 때 어떻게 처리할지는 아직 논의가 남아 있습니다.
- @IshKebab — 오류를 출력하고 컴파일을 실패시키면 되는 것 아닌가요? 다른 방법이 있나요? 깔끔하게 처리하려면 작업이 필요하겠지만, 원하는 동작은 분명해 보입니다.
- @hmokiguess — 캐시 같은 건가요? TypeScript용 Turborepo가 떠오르는데 비슷한 개념인가요?
- @boxed — 함수의 외부 API를 신뢰하고 그 아래 단계 작업을 병렬로 진행하는 방식에 가깝습니다. 함수 본문 검사에서 실패하면 컴파일을 중단하고 이미 한 작업을 버릴 수 있지만, 제 생각에는 그런 경우가 드뭅니다.
- @hmokiguess — 아, 비동기 타입 검사에 더 가깝군요.
- @saghm — 그렇게 말하니 낙관적 분기 예측과 거의 비슷하게 들립니다.
- @knuckleheads — .early-rmeta 파일에는 함수의 인자와 반환값처럼 함수 바깥쪽을 검사한 결과가 들어갑니다. 다른 크레이트가 이 정보를 받아 먼저 작업할 수 있습니다. rustc의 -Zearly-metadata가 이 기능을 켭니다. 나중에 완전한 메타데이터가 준비되면 기존 정보를 교체하는 기능도 필요합니다. rustc는 특정 지점에서 멈췄다가 다시 시작해야 하고, Cargo는 알림을 감지해 반응해야 합니다. 멈춘 프로세스를 종료하는 건 아니고 기다리게 하되, 작업 슬롯은 점유하지 않는 것으로 이해했습니다. 캐시가 아니라 작업을 멈추고 시작해 사용 가능한 슬롯을 더 잘 쓰는 방식입니다.
- @panstromek — CPU 파이프라이닝과 비슷하게 빌드 단위의 파이프라이닝을 더 깊게 하는 방식입니다. Rust에는 이미 파이프라인 빌드가 있지만, 이 방식은 그 단계를 더 앞당깁니다.
- @scoopr — 이미 비슷한 방식이 쓰이는 줄 알았는데, 조금 더 늦은 단계였던 것 같습니다. 제네릭 메서드를 바로 인스턴스화하지 않고 필요한 항목을 기록한 뒤, 별도 빌드 프로세스가 중복 인스턴스화를 막는 방법도 가능할지 궁금합니다. 중복이 얼마나 문제인지, 크레이트마다 빌드 프로필이 다른 경우 같은 복잡성은 잘 모릅니다.
- @kibwen — 이미 비슷한 부분이 있습니다. Cargo는 의존 크레이트의 메타데이터가 나올 때까지 기다리면 되므로, 의존 크레이트 전체의 컴파일 종료를 기다릴 필요는 없습니다. 이 프로토타입은 메타데이터를 타입 검사 완료 전에 기록해 기존 구조를 확장합니다. 효과는 크레이트 그래프에서 모든 코어가 이미 바쁘게 일하는지, 아니면 Cargo가 메타데이터를 기다리느라 코어를 놀리는지에 따라 달라집니다.
- @knuckleheads — 여유 코어가 거의 없거나 서로 무관한 의존성이 넓게 퍼져 있다면 효과가 거의 없습니다. 하지만 보통은 그렇지 않아서 성능이 좋아지는 경우가 있습니다.
- @quotemstr — 저장소에 패치 파일을 잔뜩 넣는 건 어리석습니다. Git에 이미 버전 관리 기능이 있는데 버전 관리 안에서 다시 버전 관리할 필요는 없습니다.
- @knuckleheads — 저장소 하나에 모든 것을 담고 싶었고, 이 방식으로 충분히 잘 됐습니다. Rust 컴파일러 팀에 보여주려고 관련 저장소를 포크하고 검토용 브랜치도 만들었습니다.
원문: Hacker News / 번역·요약: Trawling