성능 힌트 API

출시일:

Android 12 (API 수준 31) - Performance Hint 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 이상의 지연 시간을 추상화함

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

해결 방법

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

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

  1. 유사하게 동작하는 키 스레드의 힌트 세션을 만듭니다. 예:
    • 렌더링 스레드와 그 종속 항목이 세션 1개를 얻음
      1. Cocos에서 기본 엔진 스레드와 렌더링 스레드는 하나의 세션을 가져옵니다.
      2. Unity에서 Adaptive Performance Android Provider 플러그인을 통합합니다.
      3. Unreal에서 Unreal Adaptive Performance 플러그인을 통합하고 Scalability 옵션을 사용하여 여러 품질 수준을 지원합니다.
    • IO 스레드가 다른 세션을 받음
    • 오디오 스레드가 세 번째 세션을 받음
  2. 게임은 이 작업을 초기에 해야 합니다. 세션에서 시스템 리소스를 더 많이 필요로 하는 시점으로부터 최소 2밀리초(4밀리초를 넘는 것이 권장됨) 전에 해야 합니다.
  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);

Java

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);

Java

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>

Java

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);

Java

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);

Java

hintSession.updateTargetWorkDuration(targetDuration);