프로필 기반 최적화

프로필 기반 최적화(PGO)는 잘 알려진 컴파일러 최적화 기술입니다. PGO에서는 컴파일러가 프로그램 실행의 런타임 프로필을 사용하여 인라인 처리와 코드 레이아웃에 관해 최적의 선택을 합니다. 이렇게 하면 성능이 향상되고 코드 크기가 줄어듭니다.

다음 단계에 따라 PGO를 애플리케이션 또는 라이브러리에 배포할 수 있습니다. 1. 대표 워크로드 식별. 2. 프로필 수집. 3. 출시 빌드에서 프로필 사용.

1단계: 대표 워크로드 식별

먼저 애플리케이션의 대표 벤치마크 또는 워크로드를 식별합니다. 이 단계를 통해 워크로드에서 수집된 프로필이 코드의 핫 리전과 콜드 리전을 식별하므로 중요한 단계입니다. 프로필을 사용하면 컴파일러가 핫 리전에서 공격적인 최적화와 인라인 처리를 실행합니다. 또한 컴파일러는 성능을 저하시키면서 콜드 리전의 코드 크기를 줄일 수도 있습니다.

또한 우수한 워크로드를 식별하면 일반적으로 성능을 추적하는 데 유용합니다.

2단계: 프로필 수집

프로필 수집에서 이루어지는 3가지 단계는 계측으로 네이티브 코드 빌드, 기기에서 계측 앱 실행 및 프로필 생성, 호스트에서 프로필 병합/후처리입니다.

계측 빌드 생성

프로필은 애플리케이션의 계측된 빌드에서 1단계의 워크로드를 실행하여 수집합니다. 계측된 빌드를 생성하려면 컴파일러 및 링커 플래그에 -fprofile-generate를 추가하세요. 기본 빌드 단계에서는 플래그가 필요하지 않으므로 이 플래그는 별도의 빌드 변수로 제어해야 합니다.

프로필 생성

그런 다음 기기에서 계측된 앱을 실행하고 프로필을 생성합니다. 계측된 바이너리가 실행될 때 메모리에 프로필이 수집되고 종료 시 파일에 기록됩니다. 하지만, atexit로 등록된 함수는 Android 앱에서 호출되지 않으며 앱이 곧바로 종료됩니다.

애플리케이션/워크로드는 프로필 파일의 경로를 설정한 후 프로필 쓰기를 명시적으로 트리거하는 추가 작업을 실행해야 합니다.

  • 프로필 파일 경로를 설정하려면 __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw를 호출합니다. %m은 공유 라이브러리가 여러 개 있는 경우에 유용합니다. %m`은 라이브러리의 고유 모듈 서명으로 확장되므로 라이브러리마다 별도의 프로필이 생성됩니다. 기타 유용한 패턴 지정자는 여기를 참고하세요. PROFILE_DIR은 앱에서 쓰기 가능한 디렉터리입니다. 런타임 시 이 디렉터리를 감지하는 방법은 데모를 참고하세요.
  • 프로필 쓰기를 명시적으로 트리거하려면 __llvm_profile_write_file 함수를 호출하세요.
extern "C" {
extern int __llvm_profile_set_filename(const char*);
extern int __llvm_profile_write_file(void);
}

#define PROFILE_DIR "<location-writable-from-app>"
void workload() {
  // ...
  // run workload
  // ...

  // set path and write profiles after workload execution
  __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw");
  __llvm_profile_write_file();
  return;
}

NB: 워크로드가 독립형 바이너리인 경우 프로필 파일을 더 쉽게 생성할 수 있습니다. 바이너리를 실행하기 전에 LLVM_PROFILE_FILE 환경 변수를 %t/default-%m.profraw로 설정하기만 하면 됩니다.

프로필 후처리

프로필 파일의 형식은 .profraw입니다. 먼저 adb pull을 사용하여 기기에서 프로필을 가져와야 합니다. 가져온 후에는 컴파일러에 전달할 수 있도록 NDK의 llvm-profdata 유틸리티를 사용하여 .profraw.profdata로 변환합니다.

$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-profdata \
    merge --output=pgo_profile.profdata \
    <list-of-profraw-files>

동일한 NDK 버전의 llvm-profdataclang을 사용하면 프로필 파일 형식의 버전 불일치를 방지할 수 있습니다.

3단계: 프로필을 사용하여 애플리케이션 빌드

애플리케이션의 출시 빌드 단계에서는 컴파일러와 링커에 -fprofile-use=<>.profdata를 전달하여 이전 단계의 프로필을 사용하세요. 코드가 변화하더라도 프로필을 사용할 수 있습니다. Clang 컴파일러는 소스와 프로필 간의 약간의 불일치를 허용합니다.

NB: 일반적으로 대부분의 라이브러리는 아키텍처 간에 프로필을 공통으로 사용합니다. 예를 들어 라이브러리의 arm64 빌드에서 생성된 프로필은 모든 아키텍처에 사용할 수 있습니다. 라이브러리에 아키텍처별 코드 경로가 있는 경우(arm과 x86 또는 32비트와 64비트) 이러한 구성에 각각 별도의 프로필을 사용해야 한다는 점에 주의하세요.

종합적으로 살펴보기

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo는 앱에서 PGO를 사용하는 엔드 투 엔드 데모를 보여줍니다. 이 데모는 이 문서에서 간단히 다룬 내용의 세부정보를 추가로 제공합니다.

  • CMake 빌드 규칙은 계측을 사용하여 네이티브 코드를 빌드하는 CMake 변수 설정 방법을 보여줍니다. 빌드 변수가 설정되지 않으면 이전에 생성된 PGO 프로필을 사용하여 네이티브 코드를 최적화합니다.
  • 계측 빌드에서 pgodemo.cpp는 프로필이 워크로드 실행임을 기록합니다.
  • 프로필의 쓰기 가능 위치는 applicationContext.cacheDir.toString()을 사용하여 MainActivity.kt에서 런타임에 가져올 수 있습니다.
  • adb root를 사용하지 않고 기기에서 프로필을 가져오려면 여기에서 adb 레시피를 사용하세요.