likes
comments
collection
share

速度优化:充分利用 CPU 闲置时刻

作者站长头像
站长
· 阅读数 32

除了游戏等少数品类应用,大部分应用都不会持续以较高的水平消耗 CPU,因此在程序运行过程中,CPU 会有很多时刻都处于闲置状态,比如用户无操作,应用在后台等等。如果我们能充分利用 CPU 在闲置时刻点去提前执行或加载后续可能用到的任务及数据,便是一种很有效的提升 CPU 效率的优化方案。想要充分利用 CPU 的闲置时刻,首先需要判断出 CPU 已经闲置了,笔者在这里介绍两种方案:

  1. 通过 proc 节点文件下的 CPU 数据进行判断
  2. 通过 times 函数判断进行判断

一,proc 文件方案

在 Linux 系统上,设备和应用的大部分信息都会记录在 proc 目录下的某个文件中,比如记录了进程内存映射信息的 maps 文件,记录了 CPU 信息的 stat 文件等。我们可以通过读取 /proc/stat 文件数据获取设备 CPU 的总消耗时间,读取 /proc/{pid}/stat 文件获取某个进程号对应进程的 CPU 消耗时间。

  1. 总 CPU 消耗

在终端通过 “adb shell cat /proc/stat” 命令查看 /proc/stat 文件,它的数据如下:

cpu  125008 117667 128037 3196237 16160 18733 11734 0 0 0
cpu0 25839 24942 30963 355685 4113 6000 2280 0 0 0
cpu1 23695 27365 27443 363280 2860 3407 2416 0 0 0
cpu2 14162 10115 20652 398174 1798 2782 2451 0 0 0
cpu3 12507 9615 21847 397652 2491 3437 2433 0 0 0
cpu4 10043 11091 5759 424106 867 783 324 0 0 0
cpu5 11832 9604 5749 423895 911 690 311 0 0 0
cpu6 14558 12965 7616 415413 1614 799 410 0 0 0
cpu7 12367 11967 8005 418027 1502 832 1104 0 0 0
intr 14033565 0 0 0 0 2212446 0 138913 137167 3850 0 197 7170 0 0 0 0 56433 39773 ……
ctxt 20963274
btime 1666009901
processes 25537
procs_running 1
procs_blocked 0
softirq 5378992 18820 1620970 6861 558158 660031 0 673436 869906 0 970810

数据中 1 到 9 行数据是从系统启动到当前时刻,不同维度下累计消耗的 CPU 时间,其中第 1 行 表示所有 CPU 核心的总体累加数据,剩下为 CPU 的各个核心所对应数据, 笔者以第 1 行总体的 CPU 使用的数据来进行讲解,该行从左到右的数据说明如下:

  • cpu:表示总体的 CPU 使用情况。
  • 125008(user):用户态,也就是应用进程消耗的 CPU 时间
  • 117667(nice):用户态中高优先级进程消耗的 CPU 时间
  • 128037(ystem):系统态,也就是内核进程消耗的 CPU 时间
  • 3196237(idle):空闲态,表示 CPU 处于空闲状态的时间
  • 16160(io_wait): IO 等待时间,CPU 等待 I/O 操作完成的时间。
  • 18733(irp): 处理硬中断时间,CPU 处理硬件中断的时间。
  • 11734(soft_irp): 处理软中断时间,CPU 处理软件中断的时间。
  • 0 0 0:无效字段

从 intr 这行开始,每行表示的数据含义如下:

  • intr:系统启动以来累计的中断次数
  • ctxt:系统启动以来累计的上下文切换次数
  • btime:系统启动时长
  • processes:系统启动后所创建过的进程数量
  • procs_running:当前处于运行状态的进程数量
  • procs_blocked:当前处于等待 IO 的进程数量
  • softirq:系统启动以来 CPU 处理软中断的时间

/proc/stat 文件里面记录的这些数据对我们分析性能有很大的帮助,根据里面的数据,就能知道 CPU 的运行总时间了,只需要将第一行 CPU 数据中的 2 到 8 列数据累加起来即可 ,实现代码如下:

RandomAccessFile mProcStatFile;

long getTotalCPUCostTime(){
    // 打开/proc/stat文件
    if(mProcStatFile == null && mAppStatFile == null){
            mProcStatFile = RandomAccessFile("/proc/stat", "r");
    }else{
        //如果文件已经打开了,则将指针移到行头
        mProcStatFile.seek(0);
    }

    // 读取文件的第一行
    String procStat = mProcStatFile.readLine();
    // 按照空格拆分数据
    String[] procStats = procStat.split(" ");
    // 2,3,4,5,6,7,8项数据累加就是总 CPU 消耗时间
    return Long.parseLong(procStats[2]) + Long.parseLong(procStats[3]) +
        Long.parseLong(procStats[4]) + Long.parseLong(procStats[5]) +
        Long.parseLong(procStats[6]) + Long.parseLong(procStats[7]) +
        Long.parseLong(rocStats[8]);
}

笔者这里是用 Java 代码实现的,在一些高版本的机型上 Java 层的代码可能没有 stat 文件的读取权限,但是在 Native 层的代码是有权限的,所以大家可以在 Native 层中通过 c++ 代码来实现上述逻辑。

  1. 进程 CPU 消耗

我们接着看示例程序的 CPU 消耗,它的进程号是 19700,因此通过 “adb shell cat /proc/19700/stat” 命令,看到的对应节点文件的数据如下:

19700 (example.android_perference) S 1271 1271 0 0 -1 1077952832 179904 0 28356 0 651 310 0 0 10 -10 42 0 529919 15416832000 25731 18446744073709551615 1 1 0 0 0 0 4612 1 1073775864 0 0 0 17 4 0 0 0 0 0 0 0 0 0 0 0 0 0

该文件只有一行数据,但是该行中的数据项非常多,里面不仅包含了该进程所消耗的 CPU 数据,还包括该进程中很多性能相关的信息,这里从左到右介绍一些常用的数据,详细解释如下表:

数据项索引数据内容说明
119700进程 ID
2(com.example.android_perference)进程的名称
3S进程的状态,用一个字符表示,如 R(运行),S(睡眠),T(终止)等
41271 父进程 ID
51271进程组 ID
14651该进程处于用户态的 CPU 时间
15310该进程处于内核态的 CPU 时间
160当前进程等待子进程在用户态累计的 CPU 时间
170当前进程等待子进程在系统态累计的 CPU 时间
1810进程优先级
19-10进程 nice 值
2042线程个数
22529919进程启动总时长
2315416832000进程的虚拟内存大小,单位为字节
2425731进程独占内存+共享库,单位为页(4KB)

通过上面字段的解释可以知道,想要获取进程的 CPU 消耗时间,将 14 项的用户态 CPU 时间和 15 项的内核态 CPU 时间累加即可,代码实现如下:

RandomAccessFile mAppStatFile;
long getAppProcessCPUTime(){
    // 打开/proc/pid/stat文件
    if(mAppStatFile == null){
        mAppStatFile = 
                RandomAccessFile("/proc/" + android.os.Process.myPid() + "/stat", "r");
    }else{
        //如果文件已经打开了,则将指针移到行头
        mAppStatFile.seek(0);
    }
    
    // 读取文件的第一行
    String appStat = mAppStatFile.readLine();
    // 按照空格拆分数据
    String[] appStats = appStat.split(" ");
    // 14,15项数据相加就是进程 CPU 运行时间
    return Long.parseLong(appStats[14]) + Long.parseLong(appStats[15]);  
}
  1. CPU 闲置通知

CPU 使用率是指在一定的时间范围内,该应用进程消耗的 CPU 时间相对于 CPU 总运行时间的占比,如果占比较低,表示应用进程消耗的 CPU 较少,就说明进程处于闲置状态。有了上面的数据,就可以来判断 CPU 是否处于闲置状态了,但是这里还涉及到两个值需要我们确认:

  1. 采集的时间范围:该值不宜太短也不易太长,10 秒到 60 秒之间都可以,太短会浪费较多资源在数据采集和计算上,太久又会导致触发闲置的频率太低,我们可以根据经验和程序的业务类型来不断的调整以达到一个最优值。
  2. 使用率:对于一个 8 核 CPU 设备来说,极限满载情况下所有核都在为这个进程服务,CPU 占用率可以接近 800%,而一个 4 核 CPU 设备,极限情况下 CPU 占用率也只能接近 400%,所以对于性能高的手机,可以将使用率闲置的阈值设置的更大一些,对于性能差的手机,这个阈值就需要低一些了,因此该值也不存在一个准确值,而是需要结合程序的情况去进行调整。

笔者这里以 10 秒作为频率来进行采集,并以 30% 作为 CPU 闲置状态的阈值来进行方案的实现,代码如下:

float CPU_USAGE_IDLE_VALUE = 0.3;
//通过调度线程池实现10秒一次的周期任务
mScheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        //获取CPU的使用率
        float cpuUsage = getCpuUsage();
        if(CPUUsage < CPU_USAGE_IDLE_VALUE){
            //CPU闲置,可以执行任务了
            ……
        }
    }
}, 10, TimeUnit.SECONDS);

long beforeTotalCpuTime;
long beforeAppCpuTime;
float getCpuUsage(){
    long curTotalCpuTime = getTotalCpuCostTime();
    long curAppCpuTime = getAppProcessCpuTime();
   
    // before值为零则是第一次获取
    if(beforeAppCpuTime == 0){
        return 0;
    }
    //计算CPU使用率
    float CpuUsage = (curTotalCpuTime - beforeTotalCpuTime)/
        (float)(curAppCpuTime - beforeAppCpuTime);
   
    beforeTotalCpuTime= curTotalCpuTime ;
    beforeAppCpuTime= curAppCpuTime ;
    
    return CpuUsage; 
}

当 CPU 使用率小于阈值时,我们就可以进行预创建页面的组件,预拉取数据,预创建次级页面的关键对象等逻辑操作,但是所有的限制任务都放在这个 if 判断条件里面做会导致业务代码耦合,所以我们可以通过观察者模式,将 CPU 闲置的状态通知给各个业务,然后在业务模块内部去进行预加载等逻辑操作。

二,Times 函数方案

通过读取并解析 stat 文件的方式是有一定的性能损耗的,特别是在高频调用的场景下,该方式所带来的性能损耗可能是无法接受的,因此当面对需要高频进行 CPU 闲置判断场景时,我们可以采用 times 函数来判断进程 CPU 是否闲置。times 函数是一个系统函数,通过在 Native 层引入 sys/times.h 文件既能调用该函数了,函数如下。

clock_t times(struct tms *buf);
struct tms {
    clock_t tms_utime; // 用户CPU时间 
    clock_t tms_stime; // 系统CPU时间 
    clock_t tms_cutime; // 子进程用户CPU时间 
    clock_t tms_cstime; // 子进程系统CPU时间 
}

times 函数会将返回的数据放在 tms 结构体中,通过 tms 结构体中的 tms_utime 和 tms_stime 参数,我们就能知道当前进程的 CPU 消耗时间了,实现代码如下:

#include <sys/times.h>

float getCPUTimes(JNIEnv *env) {
    struct tms currentTms;
    times(&currentTms);
    return currentTms.tms_utime + currentTms.tms_stime;
}

在计算进程的 CPU 使用率时,进程在单位时间内消耗的 CPU 时间为分子,CPU 总消耗时间为分母。这里我们通过 times 函数拿到了应用消耗的 CPU 时间,但是却没法拿到 CPU 总消耗时间,由于系统并没有对应的接口可以直接拿到 CPU 总消耗时间,因此还是只能通过读取并解析 stat 文件来进行。所以此时我们可以换一个指标,用进程的 CPU 使用速率来替代 CPU 使用率,并进行是否闲置的判断。

CPU 速率指的是 CPU 在某个时间段内被使用的负载程度,可以通过公式 “时间间隔内进程消耗的 CPU 时间 / (时间间隔×单位时间内 CPU 时钟滴答频率)” 来计算,公式中的分母“时间间隔×单位时间内 CPU 时钟滴答频率”,就表示这段时间间隔内,CPU 在理论上的满载时间。我们可以通过 sysconf(_SC_CLK_TCK) 函数拿到每秒的 CPU 时钟滴答频率,代码如下。

#include <bits/sysconf.h>

int getCpuTick(JNIEnv *env) {
    return sysconf(_SC_CLK_TCK);
}

Times 函数中获取的进程消耗的 CPU 时间是以秒为单位的,sysconf(_SC_CLK_TCK) 函数中返回的也是每秒的 CPU 时钟频率,所以在统计进程的 CPU 使用速率时,也需要以秒为频率进行统计,方案实现如下。

float beforeAppTime  =  0;
long beforeSysTime = 0;
float CPU_SPEED_IDLE_VALUE = 0.1;
//计算10秒内的CPU使用速率
mScheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        //执行上面的逻辑
        float curAppTime = getCPUTimes();
        long curSysTime = System.CurrentTime()/1000;
        float CpuSpeed =  (beforeAppTime - curAppTime) / 
                ((curSysTime - beforeSysTime) * getCpuTick())
        if(CpuSpeed < CPU_SPEED_IDLE_VALUE){
            //CPU闲置,可以执行任务了
            ……
        }
        beforeAppTime  = curAppTime; 
        beforeSysTime = curSysTime ;
    }
}, 10, TimeUnit.SECONDS);

当应用处于闲置状态时,CPU 的速率基本在 0.1 以下,实际场景中我们可以根据应用的特性,并通过经验值设定一个闲置判定的阈值。通过 Times 函数来计算 CPU 速率,以此判断 CPU 是否已经闲置的方案实现起来更加简单,并且性能也更好,因此更适用于高频判断 CPU 状态的场景。

转载自:https://juejin.cn/post/7374616167761444890
评论
请登录