浅谈小米的 FEAS 技术

浅谈小米的 FEAS 技术


什么是FEAS? #

注:本文中所提及的FEAS,皆指小米的FEAS,不代指任何其他与之类似的技术。

先简单介绍一下FEAS:根据帧生成时间(或帧生成时间间隔)来实时动态调度CPU频率,在保证达到目标帧率的前提下不断尝试降低CPU频率,如果发生卡顿或掉帧则提高CPU频率。

传统的CPU调度方式有古老的interactive,比较新的schedutil,以及最新的walt。本人知识水平有限就不对它们做详细介绍了,感兴趣的请自行查阅相关资料。

FEAS是如何实现的? #

既然FEAS是根据帧生成时间来进行调度的,那么首先我们得让系统能够拿到帧生成时间。

在这里我不会花篇幅去讲Android是如何渲染画面的,也不会讲要获取帧生成时间应该从哪里下手,感兴趣的花可以去读一读 LibXZR的这篇文章

MIUI是通过 /system_ext/lib64/libmigui.so 来获取帧生成时间的。(存疑)

当然,对于“不支持FEAS”的设备,MIUI就没必要获取帧生成时间了。所以,小米很机智地在libmigui.so中设置了机型验证。

/images/e44/p1.png

系统在拿到帧生成时间之后,通过ioctl把数据报告给 /proc/perfmgr/perf_ioctl,之后内核层的perfmgr依此进行调度。

注:联发科设备通过fpsgo来实现内核层的CPU调度,高通设备则使用perfmgr,在此我们只探讨perfmgr。

综上所述,要想实现FEAS,需要系统和内核共同配合。

perfmgr是如何工作的? #

首先,FEAS是小米的秘密,小米从来没有开源过FEAS的源代码,但是之前Micode不小心泄露过(联发科的fpsgo版本)。在此我们将结合之前泄露的代码以及IDA来了解一下perfmgr的内部实现。

让我们把Redmi K50至尊版(diting)的 perfmgr.ko 导入到IDA进行分析。

/images/e44/p2.png

/sys/module/perfmgr/parameters/perfmgr_enable 为真值(非0)时,perfmgr开始工作。

perfmgr的核心工作逻辑在 perfmgr_do_policy 函数,在该函数中,perfmgr将根据接收到的帧生成时间数据以及设定的目标帧率来检查掉帧情况有没有发生,并动态调整 set_freq_level

/images/e44/p3.png

你可能会很好奇 set_freq_level 这个变量有什么用,事实上,该变量定义了perfmgr应该设置的CPU频率挡位,所谓的挡位是对应着perfmgr内置的一套频率表,以diting的perfmgr模块为例,它定义了两组频率表,一组是对应于大核集群(cpu4-cpu6)的 cpufreq_table4,另一组是对应于超大核集群(cpu7)的 cpufreq_table7

/images/e44/p4.png

在IDA看汇编代码有点困难,让我们把它翻译成C代码:

int cpufreq_table4[40] = {
    2572800, 2572800, 2572800, 2572800,
    2457600, 2457600, 2457600,
    2342400, 2342400, 2342400,
    2227200, 2227200,
    2112000, 2112000, 2112000,
    1996800, 1996800,
    1881600, 1881600,
    1766400, 1766400,
    1651200, 1651200,
    1555200, 1555200,
    1440000, 1440000,
    1324800, 1324800,
    1209600, 1209600,
    1113600, 1113600,
    998400,  998400,
    883200,  883200,
    768000,  768000,
    633600
};

int cpufreq_table7[40] = {
    2822400, 2822400,
    2707200, 2707200,
    2592000, 2592000, 2592000,
    2476800, 2476800, 2476800,
    2361600, 2361600,
    2246400, 2246400, 2246400,
    2131200, 2131200,
    1996800, 1996800, 1996800,
    1881600, 1881600,
    1766400, 1766400,
    1651200, 1651200,
    1536000, 1536000,
    1401600, 1401600,
    1286400, 1286400,
    1171200, 1171200,
    1036800, 1036800,
    921600,  921600,
    787200,  787200
};

这两组频率表是整形数组类型,都有40个元素。

那么,当 set_freq_level 变量发生变化时,将会调用 do_frame_limit_freq 方法,将大核集群和超大核集群的CPU频率分别设置为 cpufreq_table4 数组和 cpufreq_table7 数组中的第 set_freq_level 个元素的值:

/images/e44/p5.png

可以看出,挡位(set_freq_level)越低,CPU频率越高。

cpufreq_table4cpufreq_table7 中定义的CPU频率都是8+gen1的大核集群和超大核集群的可用频率,但不是全部,比如:cpufreq_table4 中的最高频率为 2572800,但8+gen1大核集群可运行的最高频率为 2745600cpufreq_table7 中的最高频率为 2822400,但8+gen1超大核集群可运行的最高频率为 2995200(满血版8+gen1可以达到3.2GHz)。之所以这么设计,应该是为了节能吧。

值得一提的是,cpufreq_table4cpufreq_table7 这两组频率表的定义也是很严谨的,你可以生成一张折线图看看:

/images/e44/p6.png

至于 perfmgr_do_policy 函数调整 set_freq_level 的具体逻辑是什么,说实话,这很复杂,我也没有完全搞清楚。

明白了perfmgr调整CPU频率的规律之后,你就能明白为什么FEAS生效时的一大特征是CPU大核和超大核频率呈阶梯状同升同降了,就像这样:

/images/e44/p7.png

FEAS效果如何? #

首先要清楚:FEAS的首要目的是稳帧,次要目的才是降低功耗。

为什么我会这么说?如果你已经理解了上一节的内容,那就让我们设想一下:此时有一个对性能要求极高的游戏,如果你启用了FEAS,那么大核集群和超大核集群将工作在什么频率?答案自然是都工作在最高频率,且一直维持在最高频率。如果是这样的话,那启用了FEAS之后虽然帧率能够达标,但肯定是更加费电。

所以,如果你看到类似这样的CPU频率折线图,不要担心,FEAS是生效的,只是由于画面渲染压力太大导致CPU频率居高不下。

/images/e44/p8.jpg

那么,你自然能联想到:FEAS更适合于性能要求不是非常高的游戏(对当前设备来说)。

让我们测一个“性能要求不高的游戏”试试:UiBench:

/images/e44/p9.jpg
/images/e44/p10.png

可以看到,在这种毫无压力轻轻松松就能跑满帧的场景下,大核和超大核的频率很快就降低到了最低,FEAS省电的目的就达到了。

让我们再测一个对性能要求比较高的游戏:PUBG MOBILE:

当然是跳P城啦!
当然是跳P城啦!
/images/e44/p12.png

这段折线图的前1分钟是跳伞阶段,此时游戏画面中需要渲染的元素很多,帧生成时间自然也更长,于是大核和超大核都工作在较高的频率。

落地之后,游戏画面中要渲染的元素就少了,压力下降,所以大核和超大核的频率就降下来了。游戏中遇到了需要刚枪的场景时,FEAS也会根据画面渲染压力及时升频。

总结 #

本文简单地介绍了小米的FEAS技术的实现原理和工作方式,希望能够对你有所帮助。

参考资料 #

  1. 借助 uprobe 实现对应用帧生成时间的监听 - LibXZR 的小本本
  2. TheSanty/kernel_xiaomi_xaga