리눅스를 위한 빠른 링커, Wild
(github.com/davidlattimore)- Wild는 반복 개발에서 매우 빠른 링크를 목표로 하는 링커이며, 아직 증분 링크는 구현되지 않았지만 비증분 링크만으로도 빠른 상태임
- 기존 빠른 링커인 mold는 증분 링크를 하지 않고 계획도 없다는 점이 Wild의 출발점이며, Wild는 Rust로 작성해 증분 링크의 복잡성을 다루려는 목표를 가짐
- 현재 지원 플랫폼은 Linux의 x86-64, ARM64, RISC-V, LoongArch64, PPC64LE이며, 정적 바이너리, static-PIE, 동적 링크 바이너리, 공유 객체, Rust proc-macro, 디버그 정보 등을 지원함
- GCC·Clang에서 기존 링커처럼 호출하는 drop-in replacement 방식으로 사용할 수 있고, Cargo, C/C++ 빌드 시스템, CI용 wild-action과 연결해 쓸 수 있음
- 아직 증분 링크, 더 복잡한 linker script, Mach-O, Windows 지원은 구현되지 않았으며, 실제 링크 여부는
readelf나strings로 바이너리의Linker: Wild version...문자열을 확인해야 함
Wild의 목표와 위치
- Wild는 반복 개발에서 매우 빠른 링크를 목표로 하는 링커임
- 장기 계획은 증분 링크를 구현하는 것이지만, 현재는 아직 구현되지 않음
- 증분 링크가 없어도 이미 비증분 링크 성능은 빠른 상태라고 밝힘
- mold는 이미 매우 빠르지만 증분 링크를 하지 않고, 작성자가 증분 링크를 할 계획이 없다고 밝힌 점이 Wild가 별도 링커로 만들어진 이유임
- Wild는 Rust로 작성되어 있으며, 증분 링크의 복잡성을 Rust로 다룰 수 있기를 기대함
설치 방법
- 릴리스 tarball은 releases page에서 받을 수 있으며, 압축을 풀고
wild바이너리를 PATH에 있는 위치로 복사하면 됨 cargo-binstall이 있으면 다음 명령으로 설치 가능함
cargo binstall wild-linker
- Homebrew 설치 명령은 다음과 같음
brew install wild-linker/wild/wild
- crates.io의 최신 릴리스를 빌드하려면 다음 명령을 사용함
cargo install --locked wild-linker
- Git의 최신 미릴리스 코드를 설치하려면 다음 명령을 사용함
cargo install --locked --bin wild --git https://github.com/wild-linker/wild.git wild-linker
- Nixpkgs의 안정 버전 Wild는
pkgs.useWildLinker pkgs.stdenv를 통해 사용할 수 있음 - 최신 불안정 Git 리비전은
nix/nix.md문서를 참고해야 함
기본 링커로 사용하는 방식
- Wild는 drop-in replacement로 설계되어 GCC나 Clang이 호출하는 방식으로 사용할 수 있음
- 선택지는 다음과 같음
- Clang 전용 옵션
--ld-path=wild - GCC 16.1 이상과 Clang의
-fuse-ld=wild- Clang은
ld.wild바이너리나 심볼릭 링크가 필요함
- Clang은
- 일반적으로 지원되는
-B <path><path>는wild를 가리키는ld가 들어 있는 디렉터리임
- Clang 전용 옵션
-
Rust와 Cargo
~/.cargo/config.toml에서 Clang과--ld-path=wild를 함께 사용할 수 있음
[target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-Clink-arg=--ld-path=wild"]- 또는
-fuse-ld=wild를 사용할 수 있음
[target.x86_64-unknown-linux-gnu] # linker = "clang" # Uncomment this line if your GCC is older than version 16. rustflags = ["-Clink-arg=-fuse-ld=wild"] -
C/C++ 빌드 시스템
- autotools, CMake, meson 등에서는 보통
LDFLAGS설정만으로 충분함
export LDFLAGS="${LDFLAGS} -fuse-ld=wild"- 오래된 GCC 버전에서는
wild를 가리키는ld심볼릭 링크를 만들고 해당 디렉터리를-B로 넘길 수 있음
ln -s /usr/bin/wild /tmp/ld export CFLAGS="${CFLAGS} -B/tmp" export CXXFLAGS="${CXXFLAGS} -B/tmp" export LDFLAGS="${LDFLAGS} -B/tmp"- 이런 빌드 시스템은 복잡할 수 있으므로, Wild가 실제 바이너리 링크에 사용됐는지
readelf로 확인하는 방법을 권장함
- autotools, CMake, meson 등에서는 보통
-
Illumos용 Cargo 설정
- Illumos에서는 Clang의 절대 경로를 지정하고, Wild도 절대 경로로 넘겨야 함
- Wild 절대 경로를 제공하지 않으면 GNU ld 또는 Sun ld로 조용히 위임될 수 있음
[target.x86_64-unknown-illumos] # Absolute path to clang - on OmniOS this is likely something like /opt/ooce/bin/clang. linker = "/usr/bin/clang" rustflags = [ # Will silently delegate to GNU ld or Sun ld unless the absolute path to Wild is provided. "-Clink-arg=-fuse-ld=/absolute/path/to/wild" ]
지원 상태와 미지원 항목
- 현재 지원되는 플랫폼과 아키텍처는 다음과 같음
-
x86-64 on Linux
-
ARM64 on Linux
-
RISC-V(riscv64gc) on Linux
- LoongArch64 on Linux
- 초기 지원 상태임
- PPC64LE on Linux
- 초기 지원 상태임
- 동작하는 기능은 다음과 같지만, 버그가 있을 수 있다는 단서가 붙어 있음
- 정적 링크된 non-relocatable 바이너리 출력
- 정적 링크된 position-independent 바이너리, 즉 static-PIE 출력
- 동적 링크 바이너리 출력
- 공유 객체
.so파일 출력 - Wild로 링크된 Rust proc-macro
- crates.io에서 다운로드 상위권인 대부분의 crate 테스트 통과
- 디버그 정보
- GNU jobserver 지원
- 부분적인 linker script 지원
- 자세한 내용은 linker script support matrix를 참고함
- Linker plugin LTO
- known issues가 있음
- 아직 구현되지 않은 큰 항목은 현재 우선순위에 가까운 순서로 다음과 같음
-
증분 링크
- 더 복잡한 linker script
- Mach-O 지원
- Windows 지원
-
Wild 사용 여부 확인
readelf는 binutils 패키지에서 설치할 수 있으며, 다음 명령으로.comment섹션 문자열을 확인함
readelf --string-dump .comment my-executable
- 다음과 같은 줄이 있으면 Wild로 링크된 바이너리임
Linker: Wild version 0.1.0
strings역시 binutils 패키지에서 사용할 수 있으며, 다음 명령으로도 확인할 수 있음
strings my-executable | grep 'Linker:'
벤치마크와 테스트 대상
- Wild의 목표는 최종적으로 증분 링크를 통해 매우 빠른 링커가 되는 것임
- 비증분 링크와 증분 링크가 활성화됐을 때의 초기 링크도 가능한 한 빠르게 만드는 목표를 가짐
- 모든 벤치마크는 tmpfs에 출력하는 조건에서 실행됨
- 벤치마크 실행 방법은 BENCHMARKING.md에 정리되어 있음
- 벤치마크 대상 시스템은 다음과 같음
- 예시 벤치마크에는 Chromium 링크 시간과 메모리 사용량,
librustc-driver링크 시간, Wild 자체 링크 시간, Raspberry Pi 5에서rust-analyzer링크 시간이 포함됨
Rust 코드 링크 예시와 프로젝트 정보
cargo test에서 Wild를 사용해 crate를 빌드하고 테스트하는 예시는 다음과 같음- 이 명령은
ripgrep,serde,tokio,rand,bitflags같은 몇몇 인기 crate에서 성공적으로 실행됨 wild바이너리가 PATH에 있어야 하며, GCC는 임의 링커 사용을 허용하지 않기 때문에 Clang 설치가 필요함
RUSTFLAGS="-Clinker=clang -Clink-args=--ld-path=wild" cargo test
ld.wild심볼릭 링크가wild를 가리키는 경우 다음 방식도 가능함
RUSTFLAGS="-Clinker=clang -Clink-args=-fuse-ld=wild" cargo test
- CI에서 Rust 코드 링커로 Wild를 사용하려면 wild-action을 참고함
- 기여 정보는 CONTRIBUTING.md, 고수준 설계 개요는 DESIGN.md에 있음
- Wild 관련 대화는 Zulip server에서 진행됨
- David의 블로그에는 Wild 링커의 여러 측면을 다룬 글이 많음
- 라이선스는 Apache License 2.0 또는 MIT license 중 선택 가능함
댓글과 토론
Hacker News 의견들
-
mold가 AGPL에서 MIT로 재라이선스된 뒤(mold 2.0 릴리스의 일부), 전 세계적으로 또 다른 빠른 링커를 만들 필요가 크게 줄었다고 봐서 이런 프로젝트가 나올 줄은 몰랐음
게다가 어떤 경우에는 이미 mold보다 2배 빠르다니 더 예상 밖이라, 어떻게 발전하는지 지켜볼 생각이고 작성자에게 행운을 빎- mold는 증분 링킹을 할 생각이 없어서, 다른 링커가 존재할 큰 이유가 있음
Microsoft의 링커는 수십 년 전부터 기본이 증분 링킹인데, Linux에는 아직 프로덕션 준비가 된 증분 링커가 없다는 게 좀 민망함 - 링커에서 AGPL과 MIT 차이가 왜 중요함?
- mold의 Windows 지원 상태가 어떻게 되는지 모르겠음
GitHub 이슈를 보면 작성자가 처음엔 지원하려다가 Windows 지원을 sold 링커로 옮겼고, 그런데 sold는 최근 보관 처리됐으니 결국 Windows 지원이 없는 건지, 흐름을 잘못 이해한 건지 헷갈림 - 내가 잘못 쓰는 걸 수도 있지만, 아마 써야 할 LTO를 쓰면 mold가 전혀 더 빠르지 않음
- 잠깐, GPL로 된 걸 MIT로 재라이선스하는 게 가능함?
- mold는 증분 링킹을 할 생각이 없어서, 다른 링커가 존재할 큰 이유가 있음
-
전에 살펴봤는데, 아직 프로덕션에 쓸 준비가 됐는지 모르겠음
README 기준으로는 아닌 것 같아서 여전히 mold를 쓰고 있음
macOS 사용자라면 Apple이 1~2년쯤 전에 새 링커를 냈고, 그래서 mold 작성자가 macOS 버전 작업을 중단했음
Rust에서 쓰려면config.toml에 이렇게 넣으면 됨[target.aarch64-apple-darwin]rustflags = ["-C","link-arg=-fuse-ld=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld","-C","link-arg=-ld_new",]- 작성자가 아직 프로덕션에 쓰면 안 된다고 꽤 명확히 말하고 있음
- mold도 프로덕션에는 안 쓰고, 개발용으로 씀
- 요즘은 새 링커가 이미 기본값 아닌가? 저걸 추가해도 효과가 있는지 잘 모르겠음
- Sequioa에서도 저 위치가 맞는지 확인 가능함?
명령줄 도구를 설치했는데/usr/bin/ld와/usr/bin/ld-classic만 있음
-
C/C++ 컴파일러가 링킹이라는 중간 단계를 없애고 전체 프로그램을 하나의 단위로 빌드하면 신선할 것 같음
컴파일러가 처음부터 전체 프로그램을 볼 수 있다면 LTO 자체가 필요 없음
증분 빌드를 위해 일부 빌드 산출물은 저장해야겠지만 객체 파일 형태가 아니라, 생성된 코드의 출처와 의존성을 아는 메타데이터가 필요해서 올바른 부분만 교체할 수 있어야 함
요즘 외부 라이브러리는 대개 동적으로 링크하므로 소스에서 빌드할 필요가 없고, 링커 제거가 비공개 의존성에 문제가 되지도 않음
그래도 부족하면 컴파일러가 객체 파일도 받아들이게 해서, 바이너리에 정적으로 링크해야 하는 레거시나 특수한 경우를 처리할 수 있음-
SQLite3는 모든 걸 이어 붙여 하나의 컴파일 단위로 만듦
아마 생각보다 많은 사람이 이미 이 방식을 쓰고 있음
-
-
빠른 링커에 대한 관심은 Rust 채택과 인기 때문에 크게 늘었음
적당한 크기의 정적 링크 Rust 바이너리도 릴리스 모드 컴파일의 링크 단계에서 몇 분 걸릴 수 있음(mold 사용 시)
Rust만의 문제는 아니고, 대체로 엄격한 정적 링크, LLVM이 제공하는 LTO와 BOLT 같은 고급 링크 시점 최적화, Rust 커뮤니티의 컴파일 시간 불만이 합쳐진 결과임
Rust는 LLVM과의 강한 관계, 사실상 의존성 때문에 LLVM의 링크 시점 마법이 가장 널리 채택된 언어가 됐고, C++에서도 같은 문제를 겪을 수 있지만 그때는 언어보다 도구체인 탓으로 여겨질 가능성이 큼
wild는 최적화 증분 링커가 될 가능성 때문에 한동안 지켜봤지만, 솔직히 실제로 증분 링크를 할 수 있기 전까지는 만져볼 동기가 전혀 없음- C++은 일부 컴파일러가 증분 컴파일과 증분 링킹을 지원해서 Rust보다 훨씬 빠르게 컴파일될 수 있음
또 C/C++ 생태계는 바이너리 라이브러리를 받아들이는 문화가 있어, 저장소를 클론하거나 개발 브랜치를 바꿀 때마다 세상 전체를 다시 컴파일하지 않고 자기 애플리케이션만 컴파일하면 되는 경우가 많음 - 이 문제는 Wasm으로 해결했음
바깥 애플리케이션 셸이 Wasm 비즈니스 로직을 호출하게 하면 내부 로직만 다시 컴파일하면 되고, 바깥 앱 셸은 재시작할 필요도 없음
- C++은 일부 컴파일러가 증분 컴파일과 증분 링킹을 지원해서 Rust보다 훨씬 빠르게 컴파일될 수 있음
-
2008년: GNU ld보다 빠르도록 만든 새 링커 gold
2015년쯤: gold보다 최소 2배 빠른 드롭인 대체 링커 lld
2021년: lld보다 몇 배 빠른 새 링커 mold
2025년: 새 링커 wild...
- 잘 언급되지 않지만, 이 모든 것은 실제 프로그램들이 쓰는 수많은 유용한 기능을 구현하지 않는 대가로 가능해짐
- 부정적인 말인지 긍정적인 말인지, 아니면 짧은 역사 정리인지 모르겠지만, 아직도 링커에서 더 나은 성능을 짜낼 수 있다는 사실은 꽤 고무적임
- gold는 binutils 2.44.0에서 제거될 예정이라, 공식적으로 죽은 셈임
- Windows에는 아직 초기 단계이긴 하지만 The RAD Linker도 있음
-
관련해서 오래됐지만 좋은 책으로 John Levine의 Linkers and Loaders가 있음
아래 목록의 마지막 책임https://www.johnlevine.com/books.phtml
몇 년 전에 읽었는데 꽤 흥미로웠고, 이 분야의 표준적인 책임
링크에 보이는 것처럼 다른 인기 컴퓨터 책들도 썼음 -
꽤 유망해 보임
처음부터 Rust로 되어 있고, 빠르면서 증분 링킹을 지원하는 게 목표임
Rust에서 쓰려면 아마 gcc를 링커 드라이버로 써도 가능할 듯함
프로젝트의.cargo/config.toml에:[target.x86_64-unknown-linux-gnu]rustflags = ["-C", "link-arg=-fuse-ld=wild"]곁가지지만, Rust는 왜 여기서 gcc나 clang에 꽂혀야 함? 빠진 기능이 있는 건가?
- 안타깝게도 gcc는
-fuse-ld=플래그로 임의의 링커를 받지 않음
받아들이는 링커는 bfd, gold, lld, mold뿐임
gcc가 wild를 링커로 호출하게 할 수는 있지만, 현재는 wild 링커가 들어 있는 디렉터리를 만들고 바이너리나 심볼릭 링크 이름을"ld"로 바꾼 뒤, gcc에-B/path/to/directory/containing/wild를 넘겨야 함
Rust가 링커를 직접 호출하지 않고 gcc나 clang으로 링커를 호출하는 이유는 C 컴파일러가 현재 플랫폼에서 libc와 C 런타임에 링크하기 위해 필요한 링커 플래그를 알기 때문임
예를 들면Scrt1.o,crti.o,crtbeginS.o,crtendS.o,crtn.o같은 것들임 - Rust 컴파일러가 기계어가 아니라 IR 바이트코드를 생성하기 때문임
- 안타깝게도 gcc는
-
궁금한데, 비증분 경우에 이게 mold보다 빠를 수 있는 이론적 이유가 뭘까?
“Rust라서”는 여러 가지에는 괜찮은 설명이지만, 기대 성능 이점을 설명하진 못함
“Rust가 활용하기 쉽게 해주는 낮은 난도의 병렬 처리 기회가 있어서”라면 흥미롭겠지만, 명시되어 있거나 암시되어 있지도 않음- 확실히는 모르겠고, 주로 Mold 코드베이스에 익숙하지 않기 때문임
단서 하나는 Mold가 더 빠른 할당자 mimalloc을 쓰면 약 10% 빨라진다고 들었다는 점임
Wild에서 mimalloc을 써봤지만 측정 가능한 속도 향상은 없었음
이걸 보면 Mold가 Wild보다 할당자를 더 많이 쓰는 것 같고, Wild에서는 힙 할당 횟수를 최적화하려고 분명히 노력했음
전반적으로는 설계 결정 차이라고 봄
이게 Rust와 어떻게 관련될 수 있냐면, Wild를 Rust에서 C나 C++로 포팅해도 성능은 거의 비슷할 거라고 확신함
다만 Rust에서는 빌림 검사기 덕분에 괜찮은 코드 패턴이 C나 C++에서는 함정이 될 수 있어 유지보수가 까다로울 수 있음
예전에 C++로 코딩할 때는 작은 성능 비용을 감수하고도 더 방어적으로 짰는데, Rust에서는 컴파일러가 뒤를 봐준다는 걸 알아서 훨씬 과감하게 짤 수 있음
- 확실히는 모르겠고, 주로 Mold 코드베이스에 익숙하지 않기 때문임
-
우연이네
한 시간 전에 작업 중인 C 프로젝트에서 wild, mold, ld 성능을 비교했음
2.3만 줄, 172개 파일이고,gcc+ld로는 사용자 시간이 약 23.4초,gcc+mold는 22.5초,gcc+wild는 21.8초 걸렸음
그래서 구조가 잘 잡힌 프로젝트라면 링크 시간이 그렇게 큰 문제가 아닐 수도 있다고 봄- 처음부터 새로 빌드하는 것처럼 들림
그 경우 대부분의 시간은 링킹이 아니라 코드 컴파일에 쓰임
빠른 링커의 장점은 반복 개발에서 가장 큼
즉 코드에 작은 변경을 한 뒤 다시 빌드하고 결과를 실행할 때, 컴파일러가 할 일은 보통 거의 없지만 링킹은 여전히 처음부터 수행되므로 지배적인 시간이 되기 쉬움 - 링크 시간은 작은 프로젝트가 아니라 Chrome 같은 걸 빌드할 때 중요함
- 빠른 링커는 대체로 증분 컴파일 상황에서 편집 주기를 줄이는 데 유용함
ld.lld는 어떰?
- 처음부터 새로 빌드하는 것처럼 들림
-
“이 벤치마크는 David Lattimore의 노트북(2020년형 System76 Lemur Pro)에서 실행됐고, 4코어(8스레드)와 42GB RAM을 갖고 있다”
https://news.ycombinator.com/item?id=33330499
참고로 wild가 비대하다고 말하려는 건 아님
문제가 있다면 그걸로 개발되는 소프트웨어와, 그런 소프트웨어를 쓸 사람들의 컴퓨터 쪽에 있음-
https://news.ycombinator.com/item?id=42896619
“... RAM이 16GB이고, 업그레이드할 수 없다...”
-
반쯤 농담이지만, Rust로 코딩하는 사람이라면 이미 32GB RAM은 갖고 있을 것 같음
개인적으로는 노트북을 64GB로 업그레이드하느라 문자 그대로 다른 모든 걸 희생한 게 거의 훌륭한 결정이었음
거의라고 한 이유는, RAM에 올인하지 말고 RAM과 디스플레이 둘 다에 돈을 썼어야 했기 때문임
유일한 단점은 일주일에 한 번 열린 탭을 정리하는 일이 저녁 내내 걸리는 작업이 됐다는 것임
-