车载Android运用开发中,可能会呈现一种奇葩的要求:与用户交互时运用需求全速运转,确保交互的流畅性,可是假如运用进入后台就需求怠速运转,让出更多的资源确保体系或前台运用的流畅度。那么根据这种需求,咱们需求完成一种能够动态调理运用履行功率的结构。

众所周知,当时运用最广泛的车载SOC-高通骁龙8155,选用1+3+4的8中心规划,其中大核主频为 2.96GHz,三个高功能中心主频为 2.42GHz,四个低功耗中心主频为 1.8GHz。

假如咱们能够将程序的进程线程运转在指定的CPU中心上,原则上就能够完成动态调理运用的履行功率。完成这种需求要用到一个Linux的函数—sched_setaffinity

这里的芯片规格数据源自中文互联网,与我个人接触到的骁龙SA8155P量产型的实践频率存在不小的出入。

sched_setaffinity简介

在介绍sched_setaffinity之前,需求先介绍一个新概念 – CPU 亲和性

CPU亲和性

CPU亲和性是指进程或线程在运转时倾向于在某个或某些CPU中心上履行,而不是随机或频频地在不同的中心之间切换。CPU亲和功能够进步进程或线程的功能,由于它能够运用CPU缓存的局部性,减少缓存失效和进程搬迁的开支。

CPU亲和性分为软亲和性硬亲和性

  • 软亲和性是Linux内核进程调度器的默认特性,它会尽量让进程在上次运转的CPU中心上持续运转,但不确保一定如此,由于还要考虑各个中心的负载均衡。

  • 硬亲和性是Linux内核提供给用户的API,它能够让用户显式地指定进程或线程能够运转在哪些CPU中心上,或许绑定到某个特定的中心上。

在Linux内核体系上,要设置或获取CPU亲和性,能够运用以下函数:

  • sched_setaffinity():设置进程或线程的CPU亲和性掩码,表明它能够运转在哪些中心上。

  • sched_getaffinity():获取进程或线程的CPU亲和性掩码,表明它当时能够运转在哪些中心上。

  • CPU_ZERO():操作CPU亲和性掩码的宏,用于清空某个中心是否在掩码中。

  • CPU_SET():操作CPU亲和性掩码的宏,用于设置某个中心是否在掩码中。

  • CPU_CLR():操作CPU亲和性掩码的宏,用于清除某个中心是否在掩码中。

  • CPU_ISSET():操作CPU亲和性掩码的宏,用于检查某个中心是否在掩码中。

运用办法

第一步:创立一个cpu_set_t类型的变量mask,用于表明CPU亲和性掩码。

第二步:然后运用CPU_ZEROCPU_SET宏来清空和设置mask,使得只有core对应的位为1,其他位为0。

第三步:调用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:表明mask指针指向的数据的长度,通常为sizeof(cpu_set_t);

mask:是一个指向cpu_set_t类型的指针,cpu_set_t是一个不透明的结构体,用于表明CPU亲和性掩码,需求运用一些宏来操作它,如CPU_ZERO, CPU_SET, CPU_CLR等。

sched_setaffinity函数成功时回来0,失败时回来-1,并设置errno为相应的错误码。可能的错误码有:

  • EFAULT: mask指针无效

  • EINVAL: mask中没有有用的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亲和性的demo。接下来咱们来运转测验。

运转测验

假设有两个需求密集核算的使命,分别为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()
}

情形一:不做任何处理,直接履行耗时使命

该场景下,咱们不做额定操作,线程调度选用Android内核默认的办法,得到如下成果:

【车载性能优化】将线程&进程运行在期望的CPU核心上

耗时使命分布在不同的CPU上履行,此刻CPU峰值约为207 / 600 %

【车载性能优化】将线程&进程运行在期望的CPU核心上

Task1耗时4037ms,Task2耗时4785ms

【车载性能优化】将线程&进程运行在期望的CPU核心上

情形二:将进程绑定到小中心上

该场景下,咱们运用ThreadAffinity将运用进程绑定CPU5上(在我的设备上CPU4、CPU5都是小中心)。

class MyApp: Application() {
    override fun onCreate() {
        // 注意确认你的CPU中心 大中心、小中心的标号。
        ThreadAffinity.pidToCore(android.os.Process.myPid(), 5)
        super.onCreate()
    }
}

【车载性能优化】将线程&进程运行在期望的CPU核心上

耗时使命根本聚集在CPU5上履行,此刻CPU峰值约为102 / 600 %

【车载性能优化】将线程&进程运行在期望的CPU核心上

Task1耗时18276ms,Task2耗时18272ms。能够看出这种办法尽管明显降低了CPU峰值,可是使命的履行功率也剧烈下降了。

【车载性能优化】将线程&进程运行在期望的CPU核心上

情形三:将进程、耗时使命绑定到大中心上

该场景下,将进程绑定在CPU2上,Task1、Task2分别绑定在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()
    }
}

【车载性能优化】将线程&进程运行在期望的CPU核心上

耗时使命根本聚集在CPU0和CPU1上履行,此刻CPU峰值约为193 / 600 %

【车载性能优化】将线程&进程运行在期望的CPU核心上

Task1耗时3193ms,Task2耗时3076ms。能够看出相比于Android内核的默认功能调度,手动分配中心能够获得更高的履行功率。

【车载性能优化】将线程&进程运行在期望的CPU核心上

归纳上述三种情况咱们能够得到以下结论:

  1. 进程绑定到小中心上会明显降低CPU峰值消耗,压制运用消耗体系资源,可是也会拖慢运用程序的履行功率。

  2. 线程指定到不同中心上履行,能够在尽量不进步CPU峰值的情况下,提升运用程序的履行功率。

总结

本文介绍了运用动态调理CPU亲和性的办法,原本是我个人用于车载Android运用功能优化的一种尝试,本身带有一定的「实验性」,详细的缺陷信任会在今后地运用中进一步闪现,所以目前仅供参考。

【车载性能优化】将线程&进程运行在期望的CPU核心上

请注意以下两点,第一,假如需求运用在你项目中,切记要与一切的运用开发进行协调,尽可能小规模地运用在一些对功能十分敏感的运用上,防止呈现很多运用争抢某个CPU的情况。第二,本文介绍的办法不适用于手机,由于手机厂商对于内核的修改,导致不同品牌设备间的CPU调度策略并不一致,在手机上运用可能会失效。

以上便是本篇文章一切的内容了,感谢你的阅读,期望对你有所协助。

本文中源码地址:github.com/linxu-link/…

参考资料

Linux中CPU亲和性(affinity)

CPU亲和性的运用与机制

C++功能榨汁机之CPU亲和性