Performance Hint API

출시됨:

Android 12 (API 수준 31) - 성능 힌트 API

Android 13 (API 수준 33) - NDK API의 성능 힌트 관리자

(미리보기) Android 15 (DP1) - reportActualWorkDuration()

CPU 성능 힌트를 사용하면 게임이 동적 CPU 성능 동작에 영향을 주어 요구사항에 더 잘 맞출 수 있습니다. 대부분의 기기에서 Android는 이전 요구사항에 따라 CPU 클록 속도와 워크로드의 코어 유형을 동적으로 조정합니다. 워크로드가 더 많은 CPU 리소스를 사용하면 클록 속도가 빨라지고 결국 워크로드는 더 큰 코어로 이동합니다. 워크로드에서 더 적은 리소스를 사용하면 Android는 리소스 할당을 줄입니다. ADPF를 사용하면 애플리케이션이나 게임에서 성능 및 기한에 관한 추가 신호를 보낼 수 있습니다. 이를 통해 시스템이 더 적극적으로 증가하여 성능이 개선되고 워크로드가 완료되면 클럭이 빠르게 낮아져 전력 사용량이 절약됩니다.

클럭 속도

Android 기기에서 CPU 클록 속도를 동적으로 조정하면 주파수가 코드의 성능에 영향을 미칠 수 있습니다. 동적 클록 속도를 처리하는 코드를 설계하는 것은 성능을 극대화하고 열 상태를 안전하게 유지하며 전력을 효율적으로 사용하는 데 중요합니다. 앱 코드에서 CPU 주파수를 직접 할당할 수 없습니다. 따라서, 앱이 더 높은 CPU 클록 속도에서 실행되도록 하는 일반적인 방법은 백그라운드 스레드에 비지 루프를 실행하여 워크로드가 더 많이 필요한 것처럼 보이는 것입니다. 이는 앱이 실제로 추가 리소스를 사용하지 않을 때 전력이 낭비되고 기기의 열 부하가 증가하므로 좋지 않은 방법입니다. CPU PerformanceHint API는 이 문제를 해결하기 위해 설계되었습니다. 실제 작업 시간과 타겟 작업 시간을 시스템에 알리면 Android에서 앱의 CPU 요구사항을 개략적으로 파악하고 리소스를 효율적으로 할당할 수 있습니다. 이렇게 하면 효율적인 전력 소비 수준에서 최적의 성능을 낼 수 있습니다.

코어 유형

게임이 실행되는 CPU 코어 유형은 또 다른 중요한 성능 요소입니다. Android 기기는 최근 워크로드 동작에 따라 스레드에 동적으로 할당된 CPU 코어를 변경하기도 합니다. CPU 코어 할당은 여러 코어 유형을 사용하는 SoC에서 훨씬 더 복잡합니다. 이러한 기기 중 일부에서는 더 큰 코어를 온도로 인해 지속할 수 없는 상태로 전환하지 않고 잠시 동안만 사용할 수 있습니다.

게임에서 CPU 코어 어피니티를 설정하려고 하면 안 되는 이유는 다음과 같습니다.

  • 워크로드에 가장 적합한 코어 유형은 기기 모델에 따라 다릅니다.
  • 더 큰 코어를 실행하는 지속 가능성은 SoC 및 각 기기 모델에서 제공하는 다양한 열 솔루션에 따라 다릅니다.
  • 이러한 열 상태에 미치는 환경적 영향은 코어 선택을 더 복잡하게 할 수 있습니다. 예를 들어 날씨나 휴대전화 케이스로 인해 기기의 열 상태가 바뀔 수 있습니다.
  • 코어 선택은 추가 성능 및 열 기능을 갖춘 새 기기를 수용할 수 없습니다. 따라서 기기는 게임의 프로세서 어피니티를 무시하는 경우가 많습니다.

기본 Linux 스케줄러 동작의 예

Linux 스케줄러 동작
그림 1. 거버너가 CPU 주파수를 높이거나 낮추는 데 약 200ms가 걸릴 수 있습니다. ADPF는 동적 전압 및 주파수 조정 시스템 (DVFS)과 함께 작동하여 와트당 최고의 성능을 제공합니다.

PerformanceHint API는 DVFS 지연 시간 이상을 추상화함

ADPF는 DVFS 지연 시간 이상을 추상화합니다.
그림 2. ADPF는 사용자를 대신하여 최적의 결정을 내리는 방법을 알고 있습니다.
  • 작업을 특정 CPU에서 실행해야 하는 경우 PerformanceHint API는 사용자를 대신하여 결정을 내리는 방법을 알고 있습니다.
  • 따라서 어피니티를 사용하지 않아도 됩니다.
  • 기기는 다양한 토폴로지로 제공됩니다. 전력 및 열 특성은 앱 개발자에게 노출하기에는 너무 다양합니다.
  • 실행 중인 기본 시스템에 대해 가정할 수 없습니다.

해결 방법

ADPF는 PerformanceHintManager 클래스를 제공하므로 게임에서 CPU 클럭 속도와 코어 유형의 성능 힌트를 Android에 보낼 수 있습니다. 그런 다음, OS는 SoC 및 기기 열 솔루션을 기반으로 힌트를 가장 잘 사용할 수 있는 방법을 결정할 수 있습니다. 앱이 이 API와 함께 열 상태 모니터링을 사용하는 경우 제한이 발생할 수 있는 비지 루프와 그 외 다른 코딩 기법을 사용하는 대신 OS에 더 많은 정보가 담긴 힌트를 제공할 수 있습니다.

게임에서 성능 힌트를 사용하는 방법은 다음과 같습니다.

  1. 유사한 방식으로 작동하는 키 스레드를 위해 힌트 세션을 만듭니다. 예:
    • 렌더링 스레드와 종속 항목이 한 세션을 받음
      1. Cocos에서 기본 엔진 스레드와 렌더링 스레드는 하나의 세션을 가져옵니다.
      2. Unity에서 Adaptive Performance Android Provider 플러그인을 통합합니다.
      3. Unreal에서 Unreal Adaptive Performance 플러그인을 통합하고 확장성 옵션을 사용하여 여러 품질 수준 지원
    • IO 스레드가 다른 세션을 받음
    • 오디오 스레드가 세 번째 세션을 받음
  2. 게임은 이 작업을 초기에 해야 합니다. 세션에서 시스템 리소스를 더 많이 필요로 하는 시점으로부터 최소 2ms(4ms를 넘는 것이 권장됨) 전에 해야 합니다.
  3. 각 힌트 세션에서 각 세션을 실행하는 데 필요한 시간을 예측합니다. 일반적인 시간은 프레임 간격과 같지만 워크로드가 프레임 간에 크게 달라지지 않는 경우 앱에서 더 짧은 간격을 사용할 수 있습니다.

이론을 실제로 적용하는 방법은 다음과 같습니다.

PerformanceHintManager 초기화 및 createHintSession

시스템 서비스를 사용하여 관리자를 가져오고 동일한 워크로드에서 작동하는 스레드 또는 스레드 그룹의 힌트 세션을 만듭니다.

C++

int32_t tids[1];
tids[0] = gettid();
int64_t target_fps_nanos = getFpsNanos();
APerformanceHintManager* hint_manager = APerformanceHint_getManager();
APerformanceHintSession* hint_session =
  APerformanceHint_createSession(hint_manager, tids, 1, target_fps_nanos);

자바

int[] tids = {
  android.os.Process.myTid()
};
long targetFpsNanos = getFpsNanos();
PerformanceHintManager performanceHintManager =
  (PerformanceHintManager) this.getSystemService(Context.PERFORMANCE_HINT_SERVICE);
PerformanceHintManager.Session hintSession =
  performanceHintManager.createHintSession(tids, targetFpsNanos);

필요한 경우 스레드 설정

출시됨:

Android 11 (API 수준 34)

나중에 추가해야 하는 스레드가 있는 경우 PerformanceHintManager.SessionsetThreads 함수를 사용합니다. 예를 들어 나중에 물리 스레드를 만들고 세션에 추가해야 하는 경우 이 setThreads API를 사용할 수 있습니다.

C++

auto tids = thread_ids.data();
std::size_t size = thread_ids_.size();
APerformanceHint_setThreads(hint_session, tids, size);

자바

int[] tids = new int[3];

// add all your thread IDs. Remember to use android.os.Process.myTid() as that
// is the linux native thread-id.
// Thread.currentThread().getId() will not work because it is jvm's thread-id.
hintSession.setThreads(tids);

하위 API 수준을 타겟팅하는 경우 스레드 ID를 변경해야 할 때마다 세션을 소멸시키고 새 세션을 다시 만들어야 합니다.

실제 작업 시간 보고

작업을 완료하는 데 필요한 실제 시간을 나노초 단위로 추적하고 각 주기에서 작업이 완료되면 시스템에 보고합니다. 예를 들어 렌더링 스레드용인 경우 모든 프레임에서 이를 호출합니다.

실제 시간을 안정적으로 가져오려면 다음을 사용하세요.

C++

clock_gettime(CLOCK_MONOTONIC, &clock); // if you prefer "C" way from <time.h>
// or
std::chrono::high_resolution_clock::now(); // if you prefer "C++" way from <chrono>

자바

System.nanoTime();

예를 들면 다음과 같습니다.

C++

// All timings should be from `std::chrono::steady_clock` or `clock_gettime(CLOCK_MONOTONIC, ...)`
auto start_time = std::chrono::high_resolution_clock::now();

// do work

auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end_time - start_time).count();
int64_t actual_duration = static_cast<int64_t>(duration);

APerformanceHint_reportActualWorkDuration(hint_session, actual_duration);

자바

long startTime = System.nanoTime();

// do work

long endTime = System.nanoTime();
long duration = endTime - startTime;

hintSession.reportActualWorkDuration(duration);

필요한 경우 타겟 작업 기간 업데이트

타겟 작업 기간이 변경될 때마다(예: 플레이어가 다른 타겟 FPS를 선택하는 경우) updateTargetWorkDuration 메서드를 호출하여 시스템에 알립니다. 그러면 OS가 새 타겟에 따라 리소스를 조정할 수 있습니다. 프레임마다 호출할 필요는 없으며 타겟 지속 시간이 변경될 때만 호출하면 됩니다.

C++

APerformanceHint_updateTargetWorkDuration(hint_session, target_duration);

자바

hintSession.updateTargetWorkDuration(targetDuration);