LLVM 프로젝트에서 개선 기회로 보이는 여러 설계·개발·인프라상의 문제들을 정리한다.
URL: https://www.npopov.com/2026/01/11/LLVM-The-bad-parts.html
몇 년 전, 나는 LLVM IR의 설계 이슈에 관한 블로그 글을 쓴 적이 있다. 그 이후로 이 이슈들 중 하나는 완전히 해결되었고(opaque pointers 마이그레이션), 하나는 대부분 해결되었으며(constant expression 제거), 하나는 해결을 향해 잘 진행 중이다(ptradd 마이그레이션).
이번에는 세 가지 이슈에서 멈추지 않고 좀 더 야심차게 다뤄보려고 한다. 물론 이 이슈들이 모두 같은 중요도를 가지는 것은 아니며, 중요도는 누구에게 묻느냐에 따라 달라진다. 간결함을 위해, 나는 주로 문제가 무엇인지 설명하고 가능한 해결책에 대한 논의는 대부분 생략할 것이다.
마지막으로, 이 글은 LLVM 프로젝트의 리드 메인테이너로서의 관점에서 쓰였다는 점을 밝혀둬야 할 것 같다. 이는 LLVM을 쓰지 말아야 할 이유 목록이 아니라, LLVM을 개선할 기회 목록이다.
다른 많은 오픈소스 프로젝트와 달리, LLVM은 분명히 기여자 부족을 겪고 있지 않다. 기여자는 수천 명에 달하고 분포도 비교적 평평하다(즉, 소수의 몇 사람이 대부분의 기여를 담당하는 형태가 아니다.)
LLVM이 겪는 문제는 리뷰 역량이 부족하다는 점이다. 코드를 작성하는 사람은 많은데 리뷰하는 사람은 훨씬 적다. 이는 어느 정도 놀랍지 않은데, 코드 리뷰는 코드 작성보다 더 많은 전문성을 요구하며, 리뷰어(또는 그 고용주) 입장에서 즉각적인 가치1를 제공하지 않을 수도 있기 때문이다.
리뷰 역량 부족은 기여자 경험을 나쁘게 만들고, 나쁜 변경이 코드베이스에 들어가게 되는 결과로도 이어질 수 있다. 보통은 누군가 PR을 올린 뒤 오랫동안 적절한 리뷰를 받지 못하고, 결국 그들의 동료(해당 영역의 적격 리뷰어가 아닌)가 PR을 형식적으로 승인(rubberstamp)해버리는 식으로 일이 흘러간다.
관련된 또 다른 문제는 LLVM이 다소 특이한 기여 모델을 가지고 있다는 점인데, PR 작성자가 직접 리뷰어를 요청해야 한다. 이는 새 기여자에게 특히 문제가 되며, 누구에게 요청해야 할지 모르기 때문이다. 종종 관련 리뷰어는 라벨 기반 알림 시스템 덕분에 PR을 알게 되지만, 이는 UI에서 명확히 드러나지 않고 PR이 쉽게 누락될 수 있다.
여기서의 잠재적 개선으로는 Rust 스타일의 PR 할당 시스템이 있을 수 있다.
LLVM C++ API와 LLVM IR은 안정적(stable)이지 않으며 빈번한 변경을 겪는다. 이는 LLVM의 큰 강점이자 약점이다. 강점인 이유는 LLVM이 정체되지 않고, 큰 비용이 들더라도 과거의 실수를 고치려는 의지가 있기 때문이다. 약점인 이유는 이러한 변화가 LLVM 사용자에게 비용을 강요하기 때문이다.
프런트엔드는 비교적 안정적인 C API를 사용할 수 있기 때문에 _어느 정도_는 영향을 덜 받는다. 하지만 C API가 모든 것을 덮지는 못하며, 주요 프런트엔드 대부분은 불안정한 C++ API를 사용하는 추가 바인딩을 갖고 있다.
LLVM과 더 긴밀하게 통합하는 사용자(예: 다운스트림 백엔드)는 그 선택지가 없고, 모든 API 변경을 따라가야 한다.
이는 LLVM의 전반적인 개발 철학의 일부인데, 다소 날카롭게 표현하면 “업스트림하거나 꺼져(upstream or GTFO)”라고 할 수 있다. LLVM은 관대한 라이선스를 갖고 있고 변경을 업스트림에 기여하도록 강제하지 않는다. 하지만 코드를 업스트림하지 않는다면, 업스트림의 의사결정에서도 그 코드는 고려되지 않는다.
이 항목은 다른 것들과는 조금 다른데, 여기서는 “엄밀히 더 나아지게(strictly better)” 만드는 것이 가능한지 잘 모르겠다. LLVM의 현재 안정성 스케일상의 위치가 최적이 아닐 수도 있지만, 이를 다른 곳으로 옮기면 상당한 외부효과(externalities)가 따를 것이다. 이미 LLVM에서 큰 변화를 만드는 것은 프로젝트 규모 때문에 극도로 어렵다. 여기에 추가적인 안정성 제약까지 얹으면 더 힘들어질 것이다.
LLVM은 거대한 프로젝트다. LLVM 자체만 해도 C++ 코드가 250만 줄을 넘고, 전체 모노레포는 대략 900만 줄쯤 된다. C++은 빠른 빌드 시간으로 유명하지 않으며, 그 모든 코드를 컴파일하려면 시간이 든다. 빠른 하드웨어가 있거나 빌드 팜에 접근할 수 있다면 견딜 만하지만, 저사양 노트북에서 LLVM을 빌드하려고 한다면 즐겁지 않을 것이다.
또 다른 복잡성은 디버그 정보와 함께 빌드하는 경우인데(나는 항상 추천하지 않는다), 이 경우 링크 시간이 느려지고, OOM 위험이 커지며, 디스크 사용량이 엄청나게 늘어난다. 이를 피하는 방법(공유 라이브러리 또는 dylib 빌드 사용, split dwarf 사용, lld 사용 등)이 있긴 하지만, 어느 정도의 전문 지식이 필요하다.
이 영역에서 유망한 변화로는 미리 컴파일된 헤더 사용(빌드 시간을 크게 개선함)과, 기본적으로 dylib 빌드를 사용하도록 변경(특히 디버그 정보 빌드에서 디스크 사용량 및 링크 시간을 줄임)이 있다. 또 하나로는 데몬화(daemonization)를 사용해 테스트 성능을 개선하는 것(엄밀히 “빌드 시간”은 아니지만 개발 사이클과 관련 있음)도 있다.
LLVM CI는 200개가 넘는 post-commit 빌드봇으로 구성되며, 다양한 하드웨어에서 매우 다양한 구성으로 LLVM을 테스트한다. 어떤 커밋이 빌드봇 상태를 녹색에서 빨간색으로 바꾸면, 커밋 작성자에게 이메일이 간다.
불행히도 이 CI는 결코 완전히 녹색이 아니며, 거기다 flaky하다. 이는 부분적으로 flaky 테스트(보통 lldb나 openmp) 때문이지만, 빌드봇별 이슈 때문일 수도 있다. 결과적으로 어떤 커밋이든, 완전히 무해하더라도 빌드봇 실패 알림을 받는 것이 “정상”이 되어버렸다. 이는 신호를 희석시키고, 진짜 실패를 놓치기 쉽게 만든다.
PR에 대한 사전 병합(pre-merge) 테스트의 도입은 전체 CI 상황을 크게 개선했지만, 빌드봇 문제 자체를 해결한 것은 아니다. 여기에서 진전을 이루려면 flaky 테스트/빌드봇을 더 진지하게 다루기 시작해야 한다고 생각한다.
누군가는 이것이 로켓 과학이 아니며, bors/머지 큐를 쓰면 항상 녹색을 보장할 수 있다고 말할 것이 분명하다. 하지만 이는 규모의 문제다. 일반적인 근무일에 커밋이 150개를 넘으며, 균등 분포라고 해도 10분마다 한 커밋 이상이 된다. 많은 빌드봇은 몇 시간씩 걸린다. 이를 조화시키기는 어렵다.2
어떤 면에서는 LLVM은 매우 철저한 테스트 커버리지를 갖고 있다. 우리는 새로운 최적화가 긍정/부정 테스트 모두에서 좋은 커버리지를 갖추도록 꽤 엄격하게 요구한다. 하지만 이런 테스트는 본질적으로 단일 최적화 패스나 분석에 대한 유닛 테스트다.
전체 최적화 파이프라인에 대한 커버리지는 소량(phase ordering 테스트)만 있고, 그래서 패스 상호작용 때문에 최적화가 회귀(regress)하는 경우가 있다. 미들엔드와 백엔드 파이프라인 조합에 대한 테스트는 사실상 존재하지 않는다. 여기에는 개선 여지가 있을 수 있지만, 트레이드오프가 따른다.
하지만 내가 실제로 우려하는 것은 엔드투엔드 실행 파일 테스트다. LLVM의 정식 테스트 스위트에는 이런 테스트가 전혀 없다. 실행 테스트는 별도의 llvm-test-suite 레포에 있는데, 이는 보통 일상 개발 과정에서는 사용되지 않고 빌드봇에서 실행된다. 여기에는 벤치마크부터 유닛 테스트까지 다양한 코드가 들어 있다.
그러나 llvm-test-suite에는 (LLVM lit 테스트에 비하면) 테스트 수가 꽤 적고, 기본 연산을 포괄적으로 커버하지도 못한다. 예컨대 서로 다른 float 포맷, 서로 다른 크기의 정수, 서로 다른 크기와 요소 타입의 벡터 등에 대한 연산 테스트 같은 것들 말이다.
부분적으로는 C/C++로 테스트할 때의 한계 때문이다. C는 타입 지원이 매우 이질적이며(타깃에 대해 정의된 psABI가 없는 타입을 C 컴파일러가 노출하는 것을 좋아하지 않는다). 하지만 그렇다고 이 테스트를 Zig에 위임하는 핑계가 될 수는 없다(Zig는 어디서나 모든 것을 노출하고, 이에 상응하는 테스트 커버리지를 갖고 있다).
LLVM의 미들엔드는 매우 통합되어 있는 반면, 백엔드 구현은 매우 이질적이며, (대개 성능이지만 때로는 정확성까지) 이슈를 자신이 관심 있는 백엔드에 대해서만 고치는 경향이 있다.
이는 다양한 형태로 나타난다. 예를 들어 범용 DAG 결합(combine) 대신 타깃 특화 DAG 결합을 구현하는 경우가 있다. 내가 가장 좋아하는 사례는 최적화에 대한 타깃 훅을 잔뜩 도입하는 것이다. 그 최적화가 실제로 한 타깃에만 유익해서가 아니라, 도입한 사람이 다른 타깃에서의 파장을 처리하고 싶지 않아서 그런 경우 말이다.
이는 이해할 만하다. 다른 타깃에 대한 변경을 평가할 지식이 부족할 수도 있고, 여러 다른 메인테이너와 함께 작업해야 해서 진행이 크게 느려질 수도 있다. 하지만 결과적으로 분화와 중복은 커진다.
엔드투엔드 테스트 부족은 이 문제를 악화시키는데, 엔드투엔드 테스트는 적어도 모든 연산이 크래시 없이 컴파일되고 모든 테스트된 타깃에서 올바른 결과를 내도록 하는 일종의 강제 함수(forcing function)가 되었을 것이기 때문이다.
과거에 이 문제에 대해 충분히 불평했으니 짧게 하겠다. LLVM은 느리다. 이는 JIT 유스케이스와 (Rust나 C++처럼) 엄청난 양의 IR을 만들어내는 모든 경우에 문제가 된다.
내가 컴파일 시간 추적을 시작한 이후, 표적 개선과 회귀 방지 덕분에 상황은 크게 좋아졌다. 하지만 여전히 개선 여지는 많다. LLVM은 여전히 빠르지 않으며, 그저 덜 느릴 뿐이다.
LLVM이 특히 못하는 것 중 하나는 -O0 컴파일 시간이다. 아키텍처는 최적화를 위해 최적화되어 있으며, 최적화가 전혀 일어나지 않아도 많은 비용이 남는다. 대안 백엔드인 LLVM TPDE는 자릿수(order of magnitude) 수준으로 더 나아질 수 있음을 보여준다.
컴파일 시간 동전의 다른 면은 런타임 성능이다. LLVM이 분명히 매우 중요하게 여기는 부분이다. 그래서 LLVM에 “공식” 성능 추적 인프라가 없다는 사실이 다소 놀랍다.
물론, 많은 조직이 다운스트림에서 각자의 워크로드로 LLVM 성능을 추적한다. 어떤 면에서는 이는 좋은데, SPEC 같은 합성 벤치마크보다 실세계 워크로드에 더 집중하게 해주기 때문이다. 하지만 접근하기 쉬운 공개 성능 추적이 없으면 기여자가 변경을 평가하기가 어렵다.
공정하게 말하자면, LLVM에는 LNT 인스턴스가 있다. 하지만 a) 현재 고장 나 있고, b) LNT는 최악의 UX 범죄 중 하나이며, c) 제출되는 데이터가 거의 없고, d) PR에 대해 테스트 실행을 요청한다거나 하는 것이 불가능하다.
솔직히 이 점은 이해가 가지 않는다. 나는 개인적으로 SPEC 점수에 관심이 없지만, 관심 있는 사람은 많이 안다. 그렇다면 왜 이에 대한 일급(first-class) 추적이 없는지 미스터리다.
Undef 값은 특정 집합에서 임의의 값을 취한다. 이는 초기화되지 않은 값을 모델링하기 위해 사용되며, 역사적으로는 지연된 미정의 동작(deferred undefined behavior)을 모델링하는 데도 사용되었다. 후자의 역할은 전파 규칙이 훨씬 단순하고 최적화에 더 적합한 poison 값으로 대체되었다. 그러나 undef는 오늘날까지도 초기화되지 않은 메모리에 사용된다.
undef 값에는 두 가지 주요 문제가 있다. 첫 번째는 다중 사용(multi-use) 문제다. undef 값은 사용될 때마다 서로 다른 값을 취할 수 있다. 이는 사용 횟수를 늘리는 변환이 일반적으로 유효하지 않음을 의미하며, 값의 동등성에 기반한 최적화를 할 때 주의가 필요하다는 뜻이기도 하다. undef 값의 존재 자체가 우리가 하고 싶은 최적화를 못 하게 막거나, 복잡도를 크게 증가시킨다.
두 번째 문제는 undef를 추론하기가 매우 어렵다는 점이다. 사람은 이를 이해하기 힘들고, 증명 검사기(proof-checker) 입장에서는 계산 비용이 크다.
아마도 미래에는 초기화되지 않은 메모리를 poison 값으로 표현하게 될 가능성이 크다. 하지만 이는 LLVM이 현재 메모리 내 poison을 올바르게 처리할 수 없다는 문제에 부딪힌다. 메모리 내 poison을 제대로 지원하려면 byte 타입 같은 추가 IR 기능이 필요하다.
LLVM의 대부분의 오컴파일(즉, 정확성 버그)은 빠르게 해결되지만, 오랫동안 알려져 있음에도 아직 고쳐지지 않은 것들도 꽤 있다. 이런 이슈는 대개 이론적 성격이 강한(실세계 코드보다는 인위적으로 구성한 예제에서만 나타나는) 동시에 LLVM IR 설계 이슈와 맞물려 있다.
그중 일부는 이 문제를 해결하려면 IR 설계를 어떻게 바꿔야 하는지 대체로 감이 오지만, 그 변화가 복잡하고, 최적화 성능을 기존 수준으로 회복하기 위해 많은 작업이 필요하다. 종종 “복잡도 절벽”이 있는데, 간단하면서 거의 올바른 방법을 선택할 수도 있고, 완전히 올바르지만 매우 복잡한 방법을 선택할 수도 있다.
또 다른 경우로는, 무엇이 어떻게 동작해야 하는지 자체를 결정하는 것이 어렵다. provenance 모델이 대표적인 예다. provenance와 정수 캐스트, 타입 퍼닝(type punning)의 상호작용은 트레이드오프가 복잡한 어려운 문제다.
하지만 언젠가는 이런 문제들을 해결해야 한다. 최근 결성된 형식 명세 워킹 그룹은 이런 문제들을 다루는 것을 목표로 한다.
최적화 컴파일러의 핵심 과제는 (예: “이 값은 음수가 아니다”, “이 덧셈은 오버플로우하지 않는다”) 같은 제약 조건을 인코딩하는 것이다. 여기에는 프런트엔드가 제공하는 제약(언어의 미정의 동작 규칙에 기반)과 컴파일러가 생성하는 제약이 모두 포함된다.
특히, 프로그램에 대한 사실을 추론할 수 있는 다양한 분석이 있지만, 최적화 과정 전반에서 이를 최신 상태로 유지하는 것은 어렵다. 이를 다루는 좋은 방법 중 하나는 사실을 IR에 직접 인코딩하는 것이다. 그 주석을 올바르게 갱신하거나 폐기하는 것은 변환의 정당성(correctness)의 일부가 된다.
LLVM에는 추가 제약을 인코딩하는 방식이 매우 많다(poison 플래그, 메타데이터, 속성(attributes), assume 등). 이들은 인코딩 가능한 정보량, 최적화 중 보존 신뢰도, 그리고 최적화에 부정적으로 영향을 줄 수 있는 정도 면에서 각각 트레이드오프가 있다. 메타데이터의 정보는 너무 자주 사라지고, assume의 정보는 너무 잘 사라지지 않는다.
“기본 환경에서 엄격하게 준수되는 IEEE 754 부동소수점”이라는 깔끔한 세계를 벗어나면 부동소수점(FP) 의미론에는 여러 이슈가 있다. 떠오르는 것 몇 가지는 다음과 같다.
LLVM은 매우 큰 프로젝트이며, 의미 있는 변화를 만드는 것은 어렵고 시간이 많이 든다. 마이그레이션은 종종 수년에 걸쳐 진행되며, 모든 코드가 옮겨질 때까지 두 가지 구현이 공존한다. 대표적인 예 두 가지는 다음과 같다.
새 패스 매니저: “새” 패스 매니저는 10년이 넘기 전에 처음 도입되었다. 그리고 약 5년 전, 미들엔드 최적화 파이프라인에서 기본으로 사용하기 시작했으며, 레거시 PM 지원은 중단되었다.
하지만 백엔드는 여전히 레거시 패스 매니저를 사용하고 있다. 코드젠(codegen)에서 새 패스 매니저를 지원하기 위한 작업이 진행 중이며, 단일 타깃에 대해 엔드투엔드로 사용할 수 있는 지점에 꽤 가까워졌다. 하지만 모든 타깃을 포팅하고 레거시 패스 매니저를 완전히 은퇴시키는 데는 여전히 꽤 시간이 걸릴 것으로 예상한다.
GlobalISel: 이는 훨씬 더 극단적인 사례다. GlobalISel은 SelectionDAG(및 FastISel)를 대체하기 위한 “새” 명령 선택기(instruction selector)다. 약 10년 전 도입되었지만, 오늘날까지도 원래 SelectionDAG를 사용하던 어떤 타깃도 GlobalISel로 완전히 마이그레이션되지 않았다. GlobalISel 전용인 새 타깃이 하나 있고, 최적화하지 않은 빌드에서 기본으로 GlobalISel을 사용하는 타깃이 하나 있긴 하다. 하지만 그 외에는 SelectionDAG가 여전히 어디서나 기본이다.
두 백엔드(AMDGPU와 AArch64)는 비교적 완전한 GlobalISel 지원을 갖고 있지만, 이를 기본으로 전환할 수 있을지/언제가 될지는 불분명하다. 큰 문제는 SDAG 쪽에 새로운 최적화가 계속 구현되고 있어 동등성(parity)을 유지하기 어렵다는 점이다.
LLVM에서 호출 규약을 다루는 거의 모든 것은 엉망이다.
호출 규약 처리 책임은 프런트엔드와 백엔드 사이에 분리되어 있다. LLVM이 이를 혼자 처리할 수 없는 좋은 이유가 있는데(LLVM IR은 지나치게 낮은 추상화 수준이라 극도로 난해한 ABI 규칙을 만족시키기 어렵다).
이는 그 자체로는 문제가 아니다. 하지만 프런트엔드와 LLVM 사이의 호출 규약 계약이 무엇인지에 대한 문서가 전혀 없고, C FFI를 올바르게 구현하는 방법은 사실상 Clang이 하는 일을 보고 그대로 따라 하는 것뿐이다(그리고 규칙이 매우 미묘할 수 있어, 그렇게 복사하면 거의 항상 오류가 생긴다).
나는 이를 고치기 위해 ABI 로어링 라이브러리를 도입하자고 제안했고, vortex73가 GSoC의 일환으로 프로토타입을 구현했다. 따라서 이 문제의 한 축은 해결로 가는 길에 잘 올라섰다.
하지만 문제는 더 있다. Rust가 특히 많이 고생해온 것 중 하나는 타깃 기능(target feature)과 호출 규약의 상호작용이다. 추가 타깃 기능을 활성화하면 호출 ABI가 바뀔 수 있는데, 추가적인 float/vector 레지스터가 인자/리턴 전달에 사용되기 시작하기 때문이다. 이는 기능이 활성화된 함수와 비활성화된 함수 사이의 호출이 서로 다른 ABI를 가정해 호환되지 않을 수 있음을 의미한다.
이상적으로는 ABI와 타깃 기능은 직교(orthogonal)해야 하며, 일부 ABI가 특정 타깃 기능을 요구한다는 점에서만 결합되어야 한다(예: FP 레지스터를 활성화하지 않고 하드 float ABI를 가질 수는 없다). 타깃 기능은 함수 단위 선택이지만, ABI는 모듈 단위여야 한다.
Loongarch나 RISC-V 같은 비교적 새로운 아키텍처는 실제로 제대로 된 ABI 설계를 가지고 있지만, 대부분의 오래된 아키텍처는 그렇지 않다. 예를 들어 현재 AArch64를 soft float ABI이지만 hard float 구현으로 타깃하는 것은 불가능하다.
이와 다소 관련된 것으로 컴파일러 빌트인/libcall 처리 문제가 있다. 이는 타깃이 네이티브로 지원하지 않는 연산을 위해 컴파일러가 방출할 수 있는 보조 함수들이다. libc(또는 libm)가 제공하는 libcalls와, libgcc, compiler-rt, compiler-builtins 같은 컴파일러 런타임 라이브러리가 제공하는 builtins를 모두 포함한다.
이에 대한 진실의 근원(source of truth)은 두 가지가 있다: TargetLibraryInfo(TLI)와 RuntimeLibcalls. 전자는 미들엔드에서 사용되며, 주로 C 라이브러리 호출을 인식하고 최적화하는 데 쓰인다(대부분 libc만 커버하고 libgcc는 거의 커버하지 않는다). 후자는 백엔드에서 사용되며, 주로 컴파일러가 어떤 libcalls를 방출할 수 있는지와 그 표기(spelling)가 무엇인지 결정하는 데 쓰인다(이는 libgcc와, LLVM intrinsic으로 커버되는 libc의 하위 집합을 포함한다).
RuntimeLibcalls의 문제는 현재 대체로 타깃 트리플만을 기반으로 동작한다는 점인데, 이는 어떤 libcalls가 사용 가능하다고 가정할지에 대해 “최소 공통 분모” 가정을 하게 만든다. 그리고 그 최소 공통 분모는 보통 libgcc다. --rtlib=compiler-rt를 사용하더라도 LLVM은 이를 실제로 알지 못하며, compiler-rt에는 있지만 libgcc에는 없는 함수를 활용할 수 없다.
이는 또한 다른 런타임 라이브러리를 위한 커스터마이즈 지점을 놓치고 있음을 의미한다. 예를 들어 Rust는 f128 접미사 libcall을 compiler-builtins를 통해 제공한다고 말하고 싶을 수 있는데, 이는 C에서 long double이 어떤 타입으로 매핑되는지에 따른 타깃 특화 이름/가용성 가정을 오버라이드해야 한다. 하지만 현재는 그런 방법이 없다.
이 영역에서는(arsenm에 의해) 많은 작업이 진행 중이므로, 가까운 미래에 상황이 개선되기를 기대한다.
LLVM에는 두 개의 상위 레벨 데이터 홀더가 있다. 모듈은 컴파일 단위(예: LTO 이전, C/C++의 단일 파일)에 해당한다. LLVM 컨텍스트는 여러 “전역” 데이터를 보관한다. 보통 스레드당 하나의 컨텍스트가 있으며, 여러 모듈이 (원칙적으로) 하나의 컨텍스트를 사용할 수 있다.
함수와 전역은 모듈에 들어가고, 상수와 타입은 컨텍스트에 들어간다. 모듈에는 데이터 레이아웃도 포함되는데, 이는 “포인터 폭이 얼마인가” 같은 중요한 타입 레이아웃 정보를 제공한다.
상수와 타입이 데이터 레이아웃에 접근할 수 없다는 사실은 지속적인 마찰 원인이다. 타입이 하나 있어도, 추가 매개변수를 모든 곳에 전파하지 않으면 그 크기를 신뢰성 있게 알 수 없다. 데이터 레이아웃이 있느냐 없느냐에 따라 완전히 분리된 서브시스템(ConstantFold vs. ConstantFolding)도 존재한다.
동시에, 이 분리가 실제로 큰 이득을 주고 있는지도 의문이다. 타입과 상수를 공유하는 것은 모듈 링크 시 직접 공유할 수 있어 다소 편리하지만, 그 한 곳에서 명시적 리매핑을 수행하는 편이 다른 모든 곳에 복잡성을 퍼뜨리는 것보다 낫다고 생각한다. 또한 이렇게 하면 현재 비트코드 왕복을 거쳐야만 가능한 cross-context 링크도 허용할 수 있다. 이론적으로 컨텍스트는 여러 모듈을 컴파일할 때 메모리 재사용을 허용할 수도 있지만, 실제로는 대개 일대일 대응인 경우가 많다고 생각한다.
조금 세부적인 이야기지만, 최근에 이 문제를 자주 마주쳐서 언급해본다.
LLVM은 루프 불변 코드 이동(LICM)을 정규화(canonicalization) 변환으로 간주한다. 이는 타깃 특화 비용 모델링 없이 루프 밖으로 명령을 항상 끌어올린다는 뜻이다. 하지만 LICM은 값의 라이브 레인지(live range)를 늘릴 수 있고, 이는 레지스터 압박을 증가시켜 많은 spill/reload로 이어질 수 있다.
여기서의 일반적인 철학은, LICM이 모든 것을 끌어올려 미들엔드 변환이 깔끔한 루프 불변 명령을 다룰 수 있게 하고, 이후 백엔드가 레지스터 압박을 정밀하게 모델링하면서 명령을 다시 루프 안으로 내려(sink) 줄 것이라는 것이다.
하지만… 두 번째 부분은 실제로 일어나지 않는다. (비-PGO 빌드에서는) 명령이 루프 안으로 다시 내려가는 경우는 레지스터 할당기의 재물질화(rematerialization)나, 특수한 sinking(보통 주소 지정 모드)으로 제한되는 것 같다. 그 범주에 들지 않는 것들에 대해서는 레지스터 압박을 줄이기 위해 루프 안으로 sinking하려는 시도가 이뤄지지 않는다.
이 목록은 완전하지 않다. 더 많은 것을 언급할 수도 있지만, 점점 더 좁은 영역으로 들어가게 될 것이다. 중요한 것 대부분을 다뤘기를 바란다 — 빠뜨린 것이 있다면 꼭 알려주시길!
전체 프로젝트 건강에 관심이 없다면, 리뷰의 주된 가치는 상호성이다. 당신이 다른 사람의 PR을 리뷰하면, 다른 사람도 당신의 PR을 더 잘 리뷰해준다.↩
Rust가 이를 조화시키는 방식은 “롤업(rollups)”(여러 PR을 사람의 큐레이션을 통해 배치로 병합)과, 상당히 다른 기여 모델의 결합이다. LLVM은 하나의 일만 하는 작은 PR들의 연속(그리고 squash merge)을 선호하는 반면, Rust는 많은 커밋을 가진 큰 PR(그리고 squash하지 않음)을 선호한다. bors 때문에 승인된 Rust PR이 병합되기까지 보통 며칠이 걸리므로, 일을 진행하려면 큰 PR이 사실상 필수다. 이는 반드시 나쁜 것은 아니지만, LLVM의 현재 방식과는 매우 다르다.↩