Performance Hint API

출시됨:

Android 12 (API 수준 31) - PerformanceHintManager

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에 더 많은 정보가 담긴 힌트를 제공할 수 있습니다.

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

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)

나중에 추가해야 하는 다른 스레드가 있는 경우 setThreads PerformanceHintManager.Session의 함수를 사용합니다. 예를 들어 나중에 물리 스레드를 만들고 세션에 추가해야 하는 경우 이 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);