[車両パフォーマンスの最適化] 必要な CPU コアでスレッドとプロセスを実行します

車載 Android アプリケーションの開発では、ユーザーと対話するとき、対話のスムーズさを確保するためにアプリケーションをフルスピードで実行する必要があるという奇妙な要件が発生することがありますが、アプリケーションがバックグラウンドに移行する場合は、アイドル速度で実行して、システムまたはフロント デスクを確保するためにより多くのリソースを解放します。この要件に基づいて、アプリケーションの実行効率を動的に調整できるフレームワークを実装する必要があります。

ご存知のとおり、現在最も広く使用されている車載 SOC - Qualcomm Snapdragon 8155 は、1+3+4 8 コア設計を採用しており、大きなコアは 2.96 GHz でクロックされ、3 つの高性能コアは 2.42 GHz でクロックされます。 、4 つの低電力コアは 2.96 GHz でクロックされ、コア周波数は 1.8 GHz です。

指定されたCPUコア上でプログラムのプロセススレッドを実行できれば、原理的にはアプリケーションの実行効率を動的に調整することができます。この要件を達成するには、Linux の関数 - を使用しますsched_setaffinity

ここでのチップ仕様データは中国のインターネットから得たものであり、私が個人的に接した量産型Snapdragon SA8155Pの実際の周波数とはかなりの差異があります。

sched_setaffinity の概要

これを導入する前に、 CPU アフィニティsched_setaffinityという新しい概念を導入する必要があります

CPU アフィニティ

CPU アフィニティとは、プロセスまたはスレッドが実行時に、異なるコア間でランダムまたは頻繁に切り替えるのではなく、1 つまたは特定の CPU コア上で実行される傾向を指します。CPU アフィニティは、CPU キャッシュの局所性を利用し、キャッシュの無効化とプロセスの移行のオーバーヘッドを削減するため、プロセスまたはスレッドのパフォーマンスを向上させることができます。

CPU アフィニティは、ソフト アフィニティハード アフィニティに分けられます

  • ソフト アフィニティは Linux カーネル プロセス スケジューラのデフォルト機能で、最後に実行された CPU コアでプロセスの実行を維持しようとしますが、各コアの負荷分散も考慮する必要があるため、これは保証されません。
  • ハード アフィニティは、Linux カーネルによってユーザーに提供される API であり、これによりユーザーは、プロセスまたはスレッドを実行できる CPU コア、または特定のコアにバインドできる CPU コアを明示的に指定できます。

Linux カーネル システムでは、CPU アフィニティを設定または取得するには、次の関数を使用できます。

  • sched_setaffinity(): プロセスまたはスレッドの CPU アフィニティ マスクを設定し、どのコアで実行できるかを示します。
  • sched_getaffinity(): プロセスまたはスレッドの CPU アフィニティ マスクを取得し、現在どのコアで実行できるかを示します。
  • CPU_ZERO(): CPU アフィニティ マスクを操作するためのマクロ。特定のコアがマスク内にあるかどうかをクリアするために使用されます。
  • CPU_SET(): CPU アフィニティ マスクを操作するためのマクロ。特定のコアがマスク内にあるかどうかを設定するために使用されます。
  • CPU_CLR(): CPU アフィニティ マスクを操作するためのマクロ。特定のコアがマスク内にあるかどうかをクリアするために使用されます。
  • CPU_ISSET(): CPU アフィニティ マスクを操作するためのマクロ。特定のコアがマスク内にあるかどうかを確認するために使用されます。

使用法

ステップ 1: cpu_set_tCPU アフィニティ マスクを表す型変数マスクを作成します。

ステップ 2: 次に、sum マクロを使用してCPU_ZEROマスクCPU_SETをクリアし、コアに対応するビットのみが 1 になり、他のビットが 0 になるように設定します。

ステップ 3:sched_setaffinity現在のスレッドの CPU アフィニティを設定する関数を呼び出します。成功した場合は 0 を返し、そうでない場合は -1 を返します。

    // cpu 亲和性掩码
    cpu_set_t mask;
    // 清空
    CPU_ZERO(&mask);
    // 设置 亲和性掩码
    CPU_SET(core, &mask);
    // 设置当前线程的cpu亲和性
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
        return -1;
    }

sched_setaffinityこの関数の原理は、プロセスまたはスレッドの CPU アフィニティ マスクを設定することで、どの CPU コアで実行できるかを指定することです。CPU アフィニティ マスクはビットマップであり、各ビットは CPU コアに対応しており、特定のビットが 1 の場合はそのコア上でプロセスまたはスレッドを実行できることを意味し、それ以外の場合は実行できません。

sched_setaffinity関数を使用すると、プロセスまたはスレッドのパフォーマンスを向上させ、異なるコア間の頻繁な切り替えを回避できます。

sched_setaffinity関数のプロトタイプは次のとおりです。

int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);

pid : 設定するプロセスまたはスレッドのIDを示し、0の場合は現在のプロセスまたはスレッドを示します。

cpusetsize : マスク ポインターが指すデータの長さを示します。通常は sizeof(cpu_set_t);

マスク: cpu_set_t 型へのポインタです。cpu_set_t は、CPU アフィニティ マスクを表すために使用される不透明な構造体です。これを操作するには、CPU_ZERO、CPU_SET、CPU_CLR などのいくつかのマクロを使用する必要があります。

sched_setaffinityこの関数は、成功した場合は 0 を返し、失敗した場合は -1 を返し、errno に対応するエラー コードを設定します。考えられるエラーコードは次のとおりです。

  • EFAULT: マスク ポインタが無効です
  • EINVAL: マスクに有効な CPU コアがありません
  • EPERM: 呼び出し元に十分な権限がありません

Androidの実装

Android アプリケーションでは、JNI を使用してsched_setaffinity関数を呼び出す必要があります。AndroidStudio を使用して NDK のデフォルト プロジェクトを作成します。Cmake スクリプトは次のとおりです。

cmake_minimum_required(VERSION 3.22.1)

project("socaffinity")

add_library(${CMAKE_PROJECT_NAME} SHARED
        native-lib.cpp)

target_link_libraries(${CMAKE_PROJECT_NAME}
        android
        log)

Native-lib のソースコードは次のとおりです。

#include <jni.h>
#include <unistd.h>
#include <pthread.h>

// 获取cpu核心数
int getCores() {
    int cores = sysconf(_SC_NPROCESSORS_CONF);
    return cores;
}

extern "C" JNIEXPORT jint JNICALL Java_com_wj_socaffinity_ThreadAffinity_getCores(JNIEnv *env, jobject thiz){
    return getCores();
}
// 绑定线程到指定cpu
extern "C" JNIEXPORT jint JNICALL Java_com_wj_socaffinity_ThreadAffinity_bindThreadToCore(JNIEnv *env, jobject thiz, jint core) {
    int num = getCores();
    if (core >= num) {
        return -1;
    }
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(core, &mask);
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1) {
        return -1;
    }
    return 0;
}

// 绑定进程程到指定cpu
extern "C"
JNIEXPORT jint JNICALL
Java_com_wj_socaffinity_ThreadAffinity_bindPidToCore(JNIEnv *env, jobject thiz, jint pid,
                                                     jint core) {
    int num = getCores();
    if (core >= num) {
        return -1;
    }
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(core, &mask);
    if (sched_setaffinity(pid, sizeof(mask), &mask) == -1) {
        return -1;
    }
    return 0;
}

次に、次に示すように、JNI 呼び出しメソッドを独立したシングルトンにカプセル化します。

object ThreadAffinity {

    private external fun getCores(): Int

    private external fun bindThreadToCore(core: Int): Int

    private external fun bindPidToCore(pid: Int, core: Int): Int

    init {
        System.loadLibrary("socaffinity")
    }

    fun getCoresCount(): Int {
        return getCores()
    }

    fun threadToCore(core: Int, block: () -> Unit) {
        bindThreadToCore(core)
        block()
    }

    fun pidToCore(pid: Int, core: Int){
        bindPidToCore(pid, core)
    }

}

上記のコードを通じて、CPU アフィニティを変更するための最も単純なデモを実装しました。次にテストを実行します。

テストの実行

集中的な計算を必要とする 2 つのタスク (Task1 と Task2) があり、ロジックは 0 から 1000000000 までの累積和を計算し、消費された時間をコンソールに出力します。テストコードは次のとおりです。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    task1()
    task2()
}

// 耗时任务1
private fun task1() {
    Thread {
        var time = System.currentTimeMillis()
        var sum = 0L
        for (i in 0..1000000000L) {
            sum += i
        }
        time = System.currentTimeMillis() - time
        Log.e("SOC_", "start1: $time")
        runOnUiThread {
            binding.sampleText.text = time.toString()
        }
    }.start()
}

// 耗时任务2
private fun task2() {
    Thread {
        var time = System.currentTimeMillis()
        var sum = 0L
        for (i in 0..1000000000L) {
            sum += i
        }
        time = System.currentTimeMillis() - time
        Log.e("SOC_", "start2: $time")
        runOnUiThread {
            binding.sampleText.text = time.toString()
        }
    }.start()
}

シナリオ 1: 時間のかかるタスクを処理せずに直接実行する

このシナリオでは、追加の操作は実行せず、スレッド スケジューリングは Android カーネルのデフォルトの方法を採用し、次の結果が得られます。

時間のかかるタスクはさまざまな CPU で実行され、CPU のピークは約 207 / 600% になります

タスク 1 には 4037 ミリ秒、タスク 2 には 4785 ミリ秒かかりました

シナリオ 2: プロセスを小さなコアにバインドする

このシナリオでは、ThreadAffinity を使用してアプリケーション プロセスを CPU5 にバインドします (私のデバイスでは、CPU4 と CPU5 は両方とも小さなコアです)。

class MyApp: Application() {

    override fun onCreate() {
        // 注意确定你的CPU核心 大核心、小核心的标号。
        ThreadAffinity.pidToCore(android.os.Process.myPid(), 5)
        super.onCreate()
    }

}

時間のかかるタスクは基本的に CPU5 に集中しており、この時の CPU ピークは約 102 / 600% です

タスク 1 には 18276 ミリ秒、タスク 2 には 18272 ミリ秒かかりましたこの方法では、CPU ピーク値は大幅に削減されますが、タスクの実行効率も大幅に低下することがわかります。

シナリオ 3: プロセスと時間のかかるタスクを大規模なコアにバインドする

このシナリオでは、プロセスは CPU2 にバインドされ、タスク 1 とタスク 2 はそれぞれ CPU0 と CPU1 にバインドされます (私のデバイスでは、CPU0 ~ CPU3 はすべて大きなコアです)。

class MyApp: Application() {

    override fun onCreate() {
        // 注意确定你的CPU核心 大核心、小核心的标号。
        ThreadAffinity.pidToCore(android.os.Process.myPid(), 2)
        super.onCreate()
    }
}
private fun start1() {
    // 将线程绑定到核心0上
    ThreadAffinity.threadToCore(0) {
        Thread {
            var time = System.currentTimeMillis()
            var sum = 0L
            for (i in 0..1000000000L) {
                sum += i
            }
            time = System.currentTimeMillis() - time
            Log.e("SOC_", "start1: $time")
            runOnUiThread {
                binding.sampleText.text = time.toString()
            }
        }.start()
    }
}

private fun start2() {
    // 将线程绑定到核心1上
    ThreadAffinity.threadToCore(1) {
        Thread {
            var time = System.currentTimeMillis()
            var sum = 0L
            for (i in 0..1000000000L) {
                sum += i
            }
            time = System.currentTimeMillis() - time
            Log.e("SOC_", "start2: $time")
            runOnUiThread {
                binding.sampleText.text = time.toString()
            }
        }.start()
    }
}

時間のかかるタスクは基本的に CPU0 と CPU1 に集中しており、このときのCPU ピーク値は約 193/600% です

2023 年 7 月 21 日 10-15-25.gif

タスク 1 には 3193 ミリ秒、タスク 2 には 3076 ミリ秒かかりましたAndroid カーネルのデフォルトのパフォーマンス スケジューリングと比較して、手動でコアを割り当てた方がより高い実行効率を達成できることがわかります。

上記の 3 つの状況に基づいて、次の結論を導き出すことができます。

  1. プロセスを小さなコアにバインドすると、ピーク時の CPU 消費量が大幅に削減され、アプリケーションによるシステム リソースの消費が抑制されますが、アプリケーションの実行効率も低下します。
  2. スレッドを別のスレッドに割り当てて実行すると、CPU のピークをできるだけ増加させることなく、アプリケーションの実行効率を向上させることができます。

要約する

この記事では、CPU アフィニティを動的に調整する方法を紹介します。これは、もともと車載 Android アプリケーションのパフォーマンスを最適化するための個人的な試みでした。やや「実験的」です。具体的な欠点は、将来のアプリケーションでさらに改善されると信じています。 , したがって、現時点では参照のみです。

次の 2 点に注意してください。まず、プロジェクトで使用する必要がある場合は、すべてのアプリケーション開発者と調整し、パフォーマンスが非常に重要な一部のアプリケーションでは、大規模な問題を防ぐために、可能な限り小規模で使用してください。特定の CPU 状況をめぐって競合するアプリケーションの数。第 2 に、この記事で紹介した方法は携帯電話には適用できません。携帯電話メーカーによるカーネルの変更により、異なるブランドのデバイス間で CPU スケジューリング戦略が不一致になり、携帯電話で使用すると失敗する可能性があるためです。

以上がこの記事の内容です。読んでいただきありがとうございます。少しでもお役に立てれば幸いです。

この記事のソースコードアドレス: https://github.com/linxu-link/SocAffinity

参考文献

Linux における CPU アフィニティ (アフィニティ)

CPU アフィニティの使用とメカニズム

C++ パフォーマンス ジューサー CPU アフィニティ

おすすめ

転載: blog.csdn.net/linkwj/article/details/131866982