개발자가 러스트를 선택하는 이유는 메모리 안전성과 정확성, 그리고 무엇보다 뛰어난 성능을 확보할 수 있기 때문이다. 그러나 러스트 컴파일러가 언제나 가장 빠른 것은 아니다. 러스트 프로그램의 규모가 커질수록, 또 의존성이 많아질수록 컴파일 시간은 점점 길어진다. 한때는 전력 질주하던 개발 프로세스가 어느새 느릿느릿한 속도로 떨어지는 상황이 벌어진다.
하지만 좋은 소식도 있다. 이런 문제는 피할 수 없는 숙명이 아니다. 러스트 툴체인에서 가장 느린 부분 상당수는 활용 방법에 따라 충분히 빠르게 만들 수 있다. 여기서는 러스트 컴파일 속도를 높이기 위한 대표적인 기법들을 정리했다. 전략과 공통된 주제별로 구성했으며, 지금 바로 러스트 프로젝트에 적용할 수 있다.
1. 기본부터 점검
기존 작업 습관을 전혀 바꾸지 않고도 가장 큰 효과를 내는 방법은 러스트 툴체인을 최신 상태로 유지하는 것이다. 이미 이를 실천하고 있는 개발자도 많겠지만, 다시 한번 강조할 필요가 있다.
주기적으로 rustup update를 실행하면 최신 버전의 컴파일러를 사용할 수 있다. 러스트 컴파일러 팀이 컴파일 과정을 지속적으로 개선하고 최적화하고 있다는 의미다. 이런 최적화는 rustc 자체에만 반영되는 것이 아니라, rustc가 기반으로 사용하는 LLVM 프레임워크 전반에 걸쳐 함께 적용된다.
다만 툴체인을 적극적으로 업데이트하는 것이 프로젝트 요구 사항에 영향을 줄 수 있어 부담스럽다면, 프로젝트에서 사용할 툴체인 버전을 특정 버전으로 고정하는 방법도 있다. 예를 들어 다음과 같이 설정할 수 있다.
[toolchain]
channel = "1.85"
2. 꼭 필요한 작업만 수행
컴퓨팅 성능과 관련해 통용되는 황금률은 단순하다. 불필요한 작업을 줄일수록 성능은 좋아진다. 이는 러스트 프로젝트를 컴파일할 때도 그대로 적용된다. 실제 개발 과정에서는 전체 컴파일을 수행할 필요가 없는 경우가 대부분이다.
전체 컴파일을 수행하는 대신 cargo check를 실행하면 필요한 대부분 검사를 처리할 수 있다. 이 명령은 실제로 코드를 컴파일하지 않아도 해당 코드가 컴파일 가능한 상태인지를 확인한다. 전자는 주로 rustc의 코드 검사 메커니즘을 활용하는 과정이며, 후자는 LLVM 기반의 빌드 과정을 거치게 되는데 이 과정은 훨씬 더 많은 시간이 소요된다.
다만 cargo check가 아직 한 번도 빌드되지 않은 의존성의 컴파일까지 건너뛰는 것은 아니다. 검사를 수행하는 데 필요한 경우라면 해당 의존성은 그대로 빌드된다. 그러나 초기 cargo check 실행으로 이 과정을 한 차례 마치고 나면, 이후에 실행하는 검사 작업은 훨씬 빠르게 완료된다.
불필요한 재빌드를 피하는 또 다른 일반적인 방법은 컴파일러 캐시 도구를 활용하는 것이다. sccache 프로젝트는 러스트 컴파일 산출물을 캐시하는 기능을 제공하며, C나 C++ 등 유사한 컴파일 방식의 주요 언어도 함께 지원한다. 다만 sccache와 같은 도구는 여러 사용자가 빌드 캐시를 공유하는 환경에서 가장 큰 효과를 낸다. 예를 들어 공유 파일 시스템을 사용하는 기업의 개발팀에서 활용할 때 유용하다. 단일 시스템에서만 사용할 경우에는, 여러 프로젝트에서 동일한 크레이트 버전의 빌드 산출물을 공유하지 않는 한 큰 이점을 얻기 어렵다.
불필요한 컴파일을 줄이는 또 다른 강력한 방법은 의존성을 동적 링크 라이브러리(dynamically linked libraries), 즉 다이내믹 라이브러리(dynlibs) 형태로 감싸는 것이다. 이 방식은 러스트 툴체인에 기본으로 포함된 기능은 아니며, cargo add-dynamic이라는 서드파티 도구를 통해 구현된다. 이 도구를 사용하면 크레이트 코드를 인라인할 수 있는 정적 컴파일의 일부 장점을 포기해야 한다. 반면 링크 과정은 크게 빨라진다. 링크 작업이 런타임으로 넘어가기는 하지만, 일반적으로 그로 인한 비용은 크지 않다. cargo add-dynamic 개발자는 폴라스(polars) 데이터 과학 라이브러리를 사례로 이 기법의 구조와 장단점을 상세히 설명한 바 있다.
3. 분할 및 정복 전략
프로젝트 규모가 클수록, 구조가 명확하게 나뉘어 있다면 전체를 다시 컴파일할 필요성은 줄어든다. 예를 들어 MVC 아키텍처로 구성된 프로젝트에서 API를 변경하지 않고 뷰 영역만 수정하는 경우라면, 변경된 부분만 다시 컴파일해도 충분하다. 프로젝트 전체를 매번 빌드할 이유는 없다.
러스트에서 이를 구현하려면 카고 워크스페이스를 활용해 프로젝트를 여러 개의 서브 크레이트로 나누는 방식이 필요하다. 기존의 단일 크레이트 프로젝트는 수동으로 리팩터링해야 하며, 가까운 곳의 LLM에게 도움을 요청하지 않는 한 이를 자동으로 처리해 주는 기본 기능은 없다. 이 방식은 코드베이스를 명확한 경계로 나눌 수 있고, 분할할 만큼 충분히 큰 프로젝트일수록 선택적 재컴파일의 효과가 크다. 이 원칙을 극단적으로 적용한 사례도 있다. 대규모 SQL 코드를 러스트로 변환하는 과정에서, 컴파일 효율을 높이기 위해 이 방식을 활용한 사례를 소개한 펠데라(Feldera) 블로그 글이 그 예다.
분할 정복 접근법의 또 다른 방법으로는 병렬 컴파일이 있다. 이는 과거처럼 수동으로 활성화해야 하는 기능이 아니라, 러스트 컴파일러에서 점차 기본 제공 기능으로 자리 잡아가고 있다. 나이틀리 채널에서 병렬 컴파일을 사용하면 더 많은 병렬 기능이 기본적으로 활성화되며, 그렇지 않은 경우에는 컴파일러 플래그 -Z threads=을 사용해 수동으로 활성화할 수 있다. 여기서 은 사용할 스레드 수를 의미한다.
다만 병렬 코드 생성을 수동으로 추가하는 데 과도하게 비중을 둘 필요는 없다. 실제로 눈에 띄는 이점이 있는 경우에만 적용하는 것이 바람직하다. 이를 확인하는 한 가지 방법은 완전히 깨끗한 상태에서 병렬도를 달리해 빌드를 실행해 보는 것이다. 예를 들어 8코어 시스템이라면 빌드 과정에서 8개 스레드와 4개 스레드를 각각 사용해 컴파일을 수행한 뒤, 성능 차이가 실제로 존재하는지 비교해 보면 된다.
4. 분석하고 또 분석하기
측정 없이 무엇을 최적화해야 할지 판단하기는 어렵다. 다행히 러스트의 cargo build에는 빌드 시간 분석을 위한 강력한 도구인 --timings 옵션이 포함돼 있다. 이를 실행하면 HTML 형식의 상세한 리포트가 생성되며, 컴파일 과정에서 정확히 어떤 단계가 시간을 잡아먹는지 파악하는 데 도움이 된다.
문제의 원인이 항상 특정 크레이트 자체에 있는 것은 아니다. 경우에 따라서는 해당 크레이트가 사용하는 프로시저 매크로가 병목이 되기도 한다. 컴파일러의 나이틀리 전용 플래그인 -Zmacro-stats를 사용하면, 특정 매크로가 실제로 몇 줄의 코드를 생성하는지에 대한 정보를 확인할 수 있다. 이를 통해 매크로 확장으로 인해 예상보다 많은 코드가 생성되고 있는지 파악할 수 있으며, 특정 크레이트의 컴파일 시간이 비정상적으로 긴 이유를 짐작하는 단서가 될 수 있다.
각 컴파일 단계에 소요되는 시간을 확인하는 것도 유용한 방법이다. -Z time-passes 컴파일러 플래그를 사용하면 LLVM 오버헤드와 링커 작업을 포함해 각 단계별 경과 시간이 표시된다. 특히 링커가 차지하는 시간을 과소평가해서는 안 된다. 링커는 종종 간과되지만, 컴파일 과정에서 중요한 병목 지점이 될 수 있다. 이 경우 링커를 바꿔보는 것도 하나의 방법이다. lld나 mold 등을 사용해 링크 시간이 실제로 개선되는지 실험해볼 수 있다.
5. 플랫폼별 최적화 활용하기
마이크로소프트 윈도우를 사용해야 하는 상황이거나 의도적으로 윈도우를 선택한 경우라면 개발용 소프트웨어 프로젝트는 개발 시나리오를 위해 새로 설계된 파일 시스템 위에 두는 것이 바람직하다. 이른바 데브 드라이브(Dev Drive)는 ReFS라는 새로운 파일 시스템을 기반으로 하며, 볼륨을 수동으로 검사할 필요가 없는 보다 적극적인 자동 복구 동작을 제공한다. 또한 윈도우의 백신 검사로 인한 간섭을 자동으로 줄여주는 특징도 있다.
리눅스 사용자는 메모리 기반의 임시 파일 시스템을 구성해 컴파일된 산출물을 빠르게 캐시하는 용도로 활용할 수 있다. 단점은 재부팅 시 해당 공간에 저장된 데이터가 모두 사라진다는 점이다. 다만 워크스테이션을 장시간 재부팅하지 않고 사용하는 환경이라면, 재부팅 이후 한 번만 캐시를 다시 구축하면 된다는 점에서 충분히 실용적인 선택이 될 수 있다.
결론
러스트 컴파일러 팀은 개발자의 별도 개입 없이도 컴파일 시간을 줄이기 위해 지속적으로 개선을 이어가고 있다. 그러나 컴파일러 자체만으로는 속도 개선의 모든 부담을 감당할 수 없는 영역도 항상 존재한다. 프로젝트에서 어떤 부분이 컴파일 병목으로 작용하는지를 보다 적극적으로 파악하고 대응할수록 러스트로 개발한 소프트웨어의 개발 속도 역시 그만큼 빨라진다.
dl-itworldkorea@foundryco.com
Serdar Yegulalp editor@itworld.co.kr
저작권자 Foundry & ITWorld, 무단 전재 및 재배포 금지
