Performance Hint 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 コアの種類も、もう 1 つの重要なパフォーマンス要因です。Android デバイスは、一般に、スレッドに割り当てられた CPU コアを最近のワークロード動作に基づいて動的に変更します。コアの種類が複数ある SoC では、CPU コアの割り当てはさらに複雑になります。これらのデバイスの一部では、大きなコアは短時間しか使用できず、温度的に持続できない状態になることはありません。

ゲームでは、次の理由により CPU コア アフィニティを設定しないようにしてください。

  • ワークロードに最適なコアの種類は、デバイスのモデルによって異なります。
  • 大きなコアの実行の持続可能性は、SoC によって異なります。また、各デバイスのモデルに用意されている各種の温度ソリューションによっても異なります。
  • 環境が温度状態に与える影響は、コアの選択をさらに複雑にする可能性があります。たとえば、天気やスマートフォン ケースにより、デバイスの温度状態が変わることがあります。
  • コアの選択は、パフォーマンス機能や温度機能が追加された新しいデバイスには適応しない可能性があります。このため、デバイスは一般に、ゲームのプロセッサ アフィニティを無視します。

Linux スケジューラのデフォルトの動作の例

Linux スケジューラの動作
図 1. ガバナーが CPU 周波数を増減するのに 200 ミリ秒程度かかる場合があります。ADPF は動的電圧および周波数スケーリング システム(DVFS)と連携して、ワットあたりのパフォーマンスを最大化します

PerformanceHint API は DVFS よりも多くのレイテンシを抽象化

DVFS よりも抽象化される ADPF のレイテンシ
図 2. ADPF が最適な判断を下す方法を熟知している
  • タスクを特定の CPU で実行する必要がある場合は、PerformanceHint API がユーザーに代わってその決定を行います。
  • したがって、アフィニティを使用する必要はありません。
  • デバイスにはさまざまなトポロジがあります。アプリ デベロッパーに公開するには、電力と温度の特性がばらつきがあります。
  • 実行している基盤システムについて何か仮定することはできません。

解決策

ADPF の PerformanceHintManager クラスを使用すると、ゲームは CPU クロック速度とコアタイプに関するパフォーマンスのヒントを Android に送信できます。OS はデバイスの SoC と温度ソリューションに基づき、そのヒントを最適に使用する方法を判断できます。アプリがこの API と温度状態のモニタリングを使用すれば、スロットリングを招く可能性があるビジーループやその他のコーディング手法を使用する代わりに、OS にさらに多くの情報を含むヒントを提供できます。

ゲームがパフォーマンスのヒントを使用する仕組みは以下のとおりです。

  1. 同様に動作する主要なスレッドのヒント セッションを作成します。たとえば、次のようになります。
    • レンダリング スレッドとその依存関係が 1 つのセッションを取得する
      1. Cocos では、メインエンジン スレッドとレンダリング スレッドが 1 つのセッションを取得します。
      2. Unity で Adaptive Performance Android Provider プラグインを統合する
      3. Unreal で Unreal Adaptive Performance プラグインを統合し、スケーラビリティ オプションを使用して複数の品質レベルをサポートします。
    • IO スレッドが別のセッションを取得する
    • オーディオ スレッドが 3 番目のセッションを取得する
  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);