往期文章:
任务调度器:从入门到放弃(一)
任务调度器:从入门到放弃(二)
上文我们讲了两种算法来量化单个任务的负载,同时遗留了一个问题,就是小任务,到底是选择大核运行还是小核运行。其实这个问题在ARM推出big-little架构的时候就考虑到了。linaro在2015年就尝试将功耗引入到scheduler的逻辑里面 https://old.linaro.org/blog/energy-aware-scheduling-eas-progress-update/
EAS全称Energy Aware Scheduling 。那么它是如何aware的呢?这里面有几个步骤。
1.假设把任务放到某个小核CPU上(到底哪个cpu呢?按照SMP的均衡原则),预测出CPU的频率
2.基于频率,计算出CPU的功耗
3.把任务放到大核上,预测出CPU频率,计算出CPU功耗。
4.把任务放到小核,预测出CPU频率,计算出CPU功耗。
5.比较大核跟小核,哪个功耗低。
我们一步一步来看。
1. 预测频率
比如将任务放到CPU1上,原始的负载用raw_util来表示,task的任务用(util_t)来表示。
那么我们需要计算出sum_util = raw_util + util_t对应的目标频率是什么。
在EAS里面,这个代码对应的是map_util_freq 函数。
freq = map_util_freq(max_util, ps->frequency, scale_cpu);
26 static inline unsigned long map_util_freq(unsigned long util,
27 unsigned long freq, unsigned long cap)
28 {
29 return freq * util / cap;30 }
直接基于线性计算,映射到一个对应的频率上去。 那么问题来了, 这是线性的吗?
答案是不确定。针对不同的场景,情况会不一样。
下图是一个应用的Render thread线程(固定负载),在不同频率下的运行时间。这条线是一条曲线,也就是说频率跟执行时间之间并不是一个线性的关系。
下图是某一款手机芯片,在跑Dhrystone的时候,频率跟DMIPS(Dhrystone Million Instructions executed Per Second)之间的关系图。这时候又是线性的(drystone几乎不访问内存,单纯的CPU计算,而且代码也缓存在cache中)
实际上,线性的情况很少,大部分情况都不是线性的,原因无他,跟CPU的设计有关(脑瓜子嗡嗡的响)。
1.1 cache miss
这里不得不提到memory hierarchy。下图是网上搜的一张图片,大体是这样。金字塔越上面,CPU访问的速度越快,但是价格越昂贵;金字塔越下面的,CPU访问速度越慢,但是价格越便宜,容量可以做得更大。
其中横坐标是size,通过巧妙的调整memory访问的size间隔,击穿前一级cache,访问次级cache。纵坐标是访问的延迟。从这条曲线中,我们就可以看到各级cache的访问的延迟。(PS:忽略掉最后下降的部分)。
因此,当CPU要访问数据的时候(比如某个指针的内容),先从Level 1 cache中去查找,如果没有找到,就从level 2的cache中去查找。以上图为例,最恶劣的情况是从DDR内存中去获取数据,访问延迟在~150ns(单位:纳秒)左右。这是一个很大的延迟了,因为现在的CPU的频率动辄是GHZ,每个cycle的时间才1ns。 所以为了访问这个DDR中的数据,CPU需要等待几百个cycle周期,在这段时间内啥也干不了。
因此不同的软件的pattern,其实最终的频率与算力的关系是完全不一样的。
下面我们一一道来。
1.2 乱序执行 out of order
在处理器中,除非出现跳转,否则的话程序是顺序执行的。那么如果出现了cache miss的话,CPU就一定要等吗?
答案是一定的。
为了提升CPU处理器的性能,硬件设计天才们设计了“乱序执行”。
乱序执行,顾名思义就是不按照先后的顺序执行。下面举个例子,因为偷懒,就直接写C语言的伪代码了。
CPU会发现a = b + c 跟d = e +f 之间没有半毛钱关系。那么谁先执行,谁后执行,并不影响最终的结果。
因此当如果a = b + c 中发生cache miss进入stall 阻塞的话,CPU可以乱序先执行下面一条d = e + f这条指令。反正闲着也是闲着。
但是这里面特别注意的一点,就是乱序执行需要保障不影响数据最后的计算结果。比如下面这条执行就不能乱序执行。
因为如果先执行c = a + 4 = 9的话(先于a = b + 5执行的话),计算结果是错误的。CPU内部维护着这些指令之间的依赖关系。
1.3 分支预测
现在的处理器为了提升性能,都采用多发射。也就是有N多个执行器可以同时执行指令。类似我们高速公路的4车道或者8车道的马路。
但是并不是说车道宽了,性能就好,因为程序之间存在依赖关系,后面的程序代码,依赖于前面执行的结果。
程序并不完全都是顺序的,中间存在着大量的分支判断,比如if else, swtich case, goto分支跳转等等。
而且最要命的时候,这些分支跳转依赖于前面的数据计算的结果。 因此不到最后一刻,你根本不知道具体是应该执行if的语句,还是else的语句。
就如果流水线的发明推动了工业革命的生产一样。流水线也推动了CPU性能的提升。以下图的coretex-A55为例,不同的指令,从取址,到译码,issue发射,等等需要8~10级流水线。
CPU处理器是多级流水线的,因此可以同时处理N多条指令(各个指令在处理不同的阶段)。因此在整个CPU的流水线里面,其实缓冲了几十条指令,虽然他们处于N多级流水线的不同阶段,这样才能最大化的发挥CPU的性能。
但是流水线提升效率的前提是,流水线是充分工作的,中间不能存在太多的空泡。
这些分支语句却极大的影响了CPU的处理性能。天才的硬件设计师又出现了,他们发明了Speculative Execution,称之为预测执行或者投机执行。
既然CPU流水线可能会出现很多空泡,而if的判断结果还没计算出来。那么倒不如预测一下,反正闲着也是闲着。如果赌对了,那就赚大了,皆大欢喜,充分了利用了CPU的流水线提升了性能。
如果预测错了呢?
这才是最要命的,不仅不能提升性能,反而会受到惩罚,带来性能的衰退(因为惩罚过于严重,现代处理器至少要保证90%+的分支预测准确性,才能保障最基本的性能不衰退)。这就跟你遇到一个丁字路口一样,你随机选了一条继续往下走,万一走错了,你还要原路退回去,这种情况,还不如在原地等着呢。CPU受到的惩罚分为两部分;
1.流水线跟cache被刷新,因为执行指令就要访问数据,访问的数据就会被放到cache里面。一些数据就会被替换。如果预测错了,那就相当于用无效的数据替换了有用的数据。而且整个流水线需要被重新刷空,因为都是无效指令。
2.浪费了功耗,这一点很容易理解。一通操作猛如虎,最后没起到任何作用,白吃大米饭。
因此在linux kernel里面,存在大量的诸如此类的代码
用于告诉CPU,你不用瞎猜了。这段代码的if的概率比较高。同样用unlikely来提示这段代码的概率不高。
关于高性能处理器的设计,再次不继续赘述,可以参考一本叫《超标量处理器设计》的书,
另外一个可以网上搜到,叫M1 Exploration
上面讲了很多,是想说明一个残酷的事实:在不同的程序执行流下,CPU的频率与提供的算力之间的关系是不固定的,而且随着软件具体执行的内容而发生变化。
2. 功耗计算
在预测了CPU的频率之后(虽然是不准确的),
那么就需要进一步计算CPU的能量消耗。那么我们就需要有一个模型能够进行CPU功耗的基本计算。它依赖于CPU能量模型(Energy Model)。
7842 /*7843 * compute_energy(): Use the Energy Model to estimate the energy that @pd would7844 * consume for a given utilization landscape @eenv. When @dst_cpu < 0, the task7845 * contribution is ignored.7846 */7847 static inline unsigned long7848 compute_energy(struct energy_env *eenv, struct perf_domain *pd,7849 struct cpumask *pd_cpus, struct task_struct *p, int dst_cpu)7850 {7851 unsigned long max_util = eenv_pd_max_util(eenv, pd_cpus, p, dst_cpu);7852 unsigned long busy_time = eenv->pd_busy_time;7853 7854 if (dst_cpu >= 0)7855 busy_time = min(eenv->pd_cap, busy_time + eenv->task_busy_time);7856 7857 return em_cpu_energy(pd->em_pd, max_util, busy_time, eenv->cpu_cap);7858 } 1495 static int __maybe_unused _get_power(struct device *dev, unsigned long *uW,1496 unsigned long *kHz)1497 {1498 struct dev_pm_opp *opp;1499 struct device_node *np;1500 unsigned long mV, Hz;1501 u32 cap;1502 u64 tmp;1503 int ret;1504 1505 np = of_node_get(dev->of_node);1506 if (!np)1507 return -EINVAL;1508 1509 ret = of_property_read_u32(np, "dynamic-power-coefficient", &cap);1510 of_node_put(np);1511 if (ret)1512 return -EINVAL;1513 1514 Hz = *kHz * 1000;1515 opp = dev_pm_opp_find_freq_ceil(dev, &Hz);1516 if (IS_ERR(opp))1517 return -EINVAL;1518 1519 mV = dev_pm_opp_get_voltage(opp) / 1000;1520 dev_pm_opp_put(opp);1521 if (!mV)1522 return -EINVAL;1523 1524 tmp = (u64)cap * mV * mV * (Hz / 1000000);1525 /* Provide power in micro-Watts */1526 do_div(tmp, 1000000);1527 1528 *uW = (unsigned long)tmp;1529 *kHz = Hz / 1000;1530 1531 return 0;1532 }
简单一句,功耗模型基于每一个频率对应电压,其具体计算公式是 P=Cfreq vol^2,其中C是一个常量。
这是经典的动态功耗的计算模型。这条公式也说明一个最基本的问题,就是CPU频率虽然高了,虽然程序执行时间段了,但是从功耗上看是不划算的(特此阐述一下,因为我遇到很多同学认为,频率高了,程序运行时间短了。因此其实功耗好像不受影响。这种想法是错误的)。
随着工艺越来越先进,从7nm到5nm,到现在的3nm。如果学过VLSI的话(超大规模集成电路)的话,就会知道,随着工艺越来越先进。三极管的沟道变得越来越窄,变得越来越不绝缘了(PS:很抱歉用了绝缘这个词来描述,不是太恰当,暂时没有找到更加合适的词语来描述。)。因此除了CPU工作时候的动态电流之外,CPU的晶体管中间还存在着静态“漏”电流。而且这个漏电流的占比,随着工艺越来越先进,在整体电流中的占比越来越大。
更加要命的是,影响漏电流的因素很多,其中一点就是温度(这也很好理解,因为温度越高,电子越活跃,漏电流就越高)。大家也可能会感觉到,手机发热的时候,耗电会很快。(功耗大,导致发热,发热导致功耗更大。 正向循环)
笔者以前在比较旧的芯片上做过一些有趣的实验。
这是某款芯片处理器,通过CPU的循环软件来构造高负载,且关闭温控(防止出现软件限频的干扰)。左图的横坐标是时间,纵坐标是CPU结温。
可以看到CPU的结温随着时间,越来越高。 右图是power monitor采集到的整体电流。随着时间,电流也越来越大。
在这里,温度对于整个SOC的漏电流的影响,达到~280ma
另外不得不提到一点,就是在apple的专利,US 9,195.291 B2里面,提到一个digital power estimator(DPE)用来预估功耗。有兴趣的同学可以看一下这篇专利,google patent上可以搜到。
笔者也尝试在跟高校合作中去尝试优化power model的精度。
这些模型都在尝试采用PMU or AMU counter去获取到更加细节的硬件运行信息(可以看到1.1. 1.2以及1.3所提到的一些硬件运行信息),来构建更加高精度的功耗模型。
power model是功耗计算的基础中的基础,决定着软件决策的准确度。当前业界还没有一个高实时、高精度的芯片的power model可供软件使用,还需要依赖大家继续努力。
回到EAS,通过上面的几个步骤,预测频率,计算功耗。得到了任务在大核跟小核上的初步功耗预估值。然后选择一个功耗低的决策结果。
看完本文讲完,估计不管是笔者还是读者,估计心里是很绝望的。因为负载的统计是基于历史的,不准确;频率的预测也不准确;功耗的模拟计算也不准确。啥都不准确, EAS到底在整啥呢。其最终的效果值得怀疑。
跟标题中的从“入门到放弃”很贴切!
你以为这就完了吗? 远未结束。