Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

프로파일 기반 최적화

rustc는 프로파일 기반 최적화(PGO)를 지원합니다. 이 장에서는 PGO가 무엇인지, 그리고 rustc에서 PGO 지원이 어떻게 구현되어 있는지 설명합니다.

프로파일 기반 최적화란 무엇입니까?

PGO의 기본 개념은 프로그램의 일반적인 실행에 관한 데이터(예: 어떤 분기를 탈 가능성이 높은지)를 수집한 다음, 이 데이터를 인라이닝, 기계어 코드 배치, 레지스터 할당 등의 최적화에 활용하는 것입니다.

프로그램 실행에 관한 데이터를 수집하는 방법에는 여러 가지가 있습니다. 하나는 프로파일러(예: perf) 안에서 프로그램을 실행하는 것이고, 다른 하나는 계측된 바이너리, 즉 데이터 수집 기능이 내장된 바이너리를 만들어 실행하는 것입니다. 일반적으로 후자가 더 정확한 데이터를 제공합니다.

rustc에서 PGO는 어떻게 구현되어 있습니까?

rustc의 현재 PGO 구현은 전적으로 LLVM에 의존합니다. 실제로 LLVM은 PGO의 여러 형태를 지원합니다.

  • perf와 같은 외부 프로파일링 도구를 사용하여 프로그램 실행에 관한 데이터를 수집하는 샘플링 기반 PGO.
  • 코드 커버리지 인프라를 사용하여 프로파일링 정보를 수집하는 GCOV 기반 프로파일링.
  • 컴파일러 프런트엔드(예: Clang)가 자신이 생성하는 LLVM IR에 계측 내장 함수를 삽입하는 프런트엔드 기반 계측(단, 1“참고“를 확인하십시오).
  • 최적화 패스 중에 LLVM이 직접 계측 내장 함수를 삽입하는 IR 수준 계측.

rustc는 마지막 방식인 IR 수준 계측만 지원합니다. 주된 이유는 이 방식이 거의 전적으로 LLVM에서 구현되어 Rust 쪽에서는 유지 관리가 거의 필요하지 않기 때문입니다. 다행히 이 방식은 가장 현대적인 방식이기도 하며 가장 좋은 결과를 냅니다.

따라서 여기서 다루는 것은 계측 기반 방식입니다. 즉, 프로파일링 데이터는 최적화 대상 프로그램을 특별히 계측한 버전이 생성합니다. 계측 기반 PGO는 컴파일 타임 구성 요소와 런타임 구성 요소, 두 가지로 이루어져 있으며, 이 둘이 어떻게 상호 작용하는지 파악하려면 전체 워크플로를 이해해야 합니다.

전체 워크플로

PGO로 최적화된 프로그램을 생성하려면 다음 네 단계를 거칩니다.

  1. 계측을 활성화하여 프로그램을 컴파일합니다(예: rustc -C profile-generate main.rs).
  2. 계측된 프로그램을 실행합니다(예: ./main). 그러면 default-<id>.profraw 파일이 생성됩니다.
  3. LLVM의 llvm-profdata 도구를 사용하여 .profraw 파일을 .profdata 파일로 변환합니다.
  4. 이번에는 프로파일링 데이터를 활용하여 프로그램을 다시 컴파일합니다(예: rustc -C profile-use=merged.profdata main.rs).

컴파일 타임 측면

위 워크플로에서 어느 단계에 있느냐에 따라 컴파일 타임에 서로 다른 두 가지 일이 일어날 수 있습니다:

계측이 포함된 바이너리 생성

앞서 언급했듯이 프로파일링 계측은 LLVM이 추가합니다. rustc는 LLVM PassManager를 생성할 때 적절한 플래그를 설정하여 LLVM이 이를 수행하도록 지시합니다.

	// `PMBR` is an `LLVMPassManagerBuilderRef`
    unwrap(PMBR)->EnablePGOInstrGen = true;
    // Instrumented binaries have a default output path for the `.profraw` file
    // hard-coded into them:
    unwrap(PMBR)->PGOInstrGen = PGOGenPath;

또한 rustc는 적절한 내보내기 수준으로 표시하여 LLVM 프로파일링 런타임의 일부 심볼이 제거되지 않도록 해야 합니다.

최적화에 프로파일링 데이터를 활용하는 바이너리 컴파일

위에서 설명한 워크플로의 마지막 단계에서는 프로그램을 다시 컴파일하며, 이때 컴파일러는 수집된 프로파일링 데이터를 사용하여 최적화 결정을 내립니다. 여기서도 rustc는 대부분의 작업을 LLVM에 맡기며, 기본적으로 LLVM PassManagerBuilder에 프로파일링 데이터를 찾을 수 있는 위치를 알려 주기만 합니다.

	unwrap(PMBR)->PGOInstrUse = PGOUsePath;

나머지는 LLVM이 처리합니다(예: 분기 가중치 설정, 함수에 cold 또는 inlinehint 표시 등).

런타임 측면

계측 기반 접근 방식에는 항상 런타임 구성 요소도 있습니다. 즉, 계측된 프로그램이 준비되면 프로파일링 데이터를 생성하기 위해 그 프로그램을 실행해야 하며, 이 프로파일링 데이터를 수집하고 저장하려면 어느 정도의 인프라가 갖춰져 있어야 합니다.

LLVM의 경우 이러한 런타임 구성 요소는 컴파일러 런타임 라이브러리인 compiler-rt에 구현되어 있으며, 계측된 모든 바이너리에 정적으로 링크됩니다. 이것의 rustc 버전은 library/profiler_builtins에서 찾을 수 있으며, 이는 기본적으로 compiler-rt의 C 코드를 Rust 크레이트로 묶은 것입니다.

profiler_builtins를 빌드하려면 rustc의 bootstrap.toml에 profiler = true를 설정해야 합니다.

PGO 테스트

PGO 워크플로는 여러 번의 컴파일러 호출에 걸쳐 있으므로 대부분의 테스트는 run-make 테스트에서 이루어집니다(관련 테스트는 이름에 pgo가 포함되어 있습니다). 예상되는 일부 계측 산출물이 LLVM IR에 나타나는지 확인하는 코드 생성 테스트도 있습니다.

추가 정보

Clang 문서에는 LLVM의 PGO에 대한 좋은 개요가 있습니다.


  1. 참고: 이제 rustc는 실험적 옵션인 -C instrument-coverage를 통해 프런트엔드 기반 커버리지 계측을 지원하지만, 현재로서는 이 커버리지 결과를 PGO에 활용하려는 시도는 이루어지지 않았습니다. ↩