출시일:
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 스케줄러 동작의 예
PerformanceHint API는 DVFS 이상의 지연 시간을 추상화함
- 작업을 특정 CPU에서 실행해야 하는 경우 PerformanceHint API는 사용자를 대신하여 결정하는 방법을 알고 있습니다.
- 따라서 어피니티를 사용할 필요가 없습니다.
- 기기에는 다양한 토폴로지가 있습니다. 전원 및 열 특성은 너무 다양하여 앱 개발자에게 노출되기 어렵습니다.
- 실행 중인 기본 시스템에 대해서는 어떤 가정도 할 수 없습니다.
해결 방법
ADPF는 PerformanceHintManager
클래스를 제공하므로 게임에서 CPU 클록 속도와 코어 유형에 관한 성능 힌트를 Android에 전송할 수 있습니다. 그런 다음, OS는 SoC 및 기기 열 솔루션을 기반으로 힌트를 가장 잘 사용할 수 있는 방법을 결정할 수 있습니다. 앱이 이 API와 함께 열 상태 모니터링을 사용하는 경우 제한이 발생할 수 있는 비지 루프와 그 외 다른 코딩 기법을 사용하는 대신 OS에 더 많은 정보가 담긴 힌트를 제공할 수 있습니다.
게임에서 성능 힌트를 사용하는 방법은 다음과 같습니다.
- 유사하게 동작하는 키 스레드의 힌트 세션을 만듭니다. 예:
- 렌더링 스레드와 그 종속 항목이 세션 1개를 얻음
- Cocos에서 기본 엔진 스레드와 렌더링 스레드는 하나의 세션을 가져옵니다.
- Unity에서 Adaptive Performance Android Provider 플러그인을 통합합니다.
- Unreal에서 Unreal Adaptive Performance 플러그인을 통합하고 Scalability 옵션을 사용하여 여러 품질 수준을 지원합니다.
- IO 스레드가 다른 세션을 받음
- 오디오 스레드가 세 번째 세션을 받음
- 렌더링 스레드와 그 종속 항목이 세션 1개를 얻음
- 게임은 이 작업을 초기에 해야 합니다. 세션에서 시스템 리소스를 더 많이 필요로 하는 시점으로부터 최소 2밀리초(4밀리초를 넘는 것이 권장됨) 전에 해야 합니다.
- 각 힌트 세션에서 각 세션을 실행하는 데 필요한 시간을 예측합니다. 일반적인 지속 시간은 프레임 간격과 같지만 워크로드가 프레임 간에 크게 달라지지 않는 경우 앱에서 더 짧은 간격을 사용할 수 있습니다.
다음은 이 이론을 실제로 적용하는 방법입니다.
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.Session
의 setThreads
함수를 사용합니다. 예를 들어 나중에 물리 스레드를 만들어 세션에 추가해야 하는 경우 이 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);