如何“优雅”的测量系统性能
时间:2021-09-22 14:56:24
手机看文章
扫描二维码
随时随地手机看文章
[导读]【说在前面的话】在之前的文章《【嵌入式秘术】相约榨干SysTick的每一滴汁水》里,我们介绍了一个以“寄居”形式(也就是在不影响用户已有SysTick应用的情况下)测量CPU性能的开源函数库perf_counter。其仓库连接如下:https://github.com/Gorgo...
【说在前面的话】
https://github.com/GorgonMeducer/perf_counter
不知不觉中,perf_counter已经经历了大大小小7个版本:
-
提高了delay_us() 的精度
-
增加了对GCC、IAR的支持
-
改进了 __cycleof__() 宏,使其支持嵌套、并不再强制绑定 printf()
如果你使用的是Arm Compiler5(armcc)或是Arm Compiler 6(armclang),移植就特别简单。你可以按照这篇文章的手把手教程在5分钟内完成部署。
【关于对GCC和IAR的支持】对于GCC和IAR来说,由于它们都不支持 Arm Compiler 5/6 所特有的 Linker语法——$Sub$$ 和 $Super$$,因此无法直接通过 Lib 的方式实现对已有SysTick应用的 “寄居”——这里就只能忍痛割爱了。这并不影响我们以源代码的形式将它们加入已有的 GCC 或是 IAR 工程。大体步骤如下:
第一步:将 perf_counter.c 和 perf_counter.h 拷贝到你的工程目录下,并将perf_counter.c 加入到编译列表中;
第二步:将 perf_counter.h 所在的路径加入到编译器的头文件搜索路径中;
第三步:perf_counter.c 依赖 CMSIS 5.4.0 及其以上版本,确保你的工程中正确的包含了对CMSIS的支持。(这里就不再赘述)。
__attribute__((used)) //!< 避免下面的处理程序被编译器优化掉void SysTick_Handler(void){ //! 这个函数来自于 perf_counter.h user_code_insert_to_systick_handler();}
然后我们在 main() 函数里初始化 perf_counter 服务:
void main(void){ system_clock_update(); //! 更新CPU工作频率 SystemCoreClock = 72000000ul //! 假设更新后的系统频率是 72MHz init_cycle_counter(false); ...} 需要特别注意的是:由于用户并没有自己初始化 SysTick,因此我们需要将这一情况告知 perf_counter 库——由它来完成对 SysTick 的初始化——这里传递 false 给函数 init_cycle_counter() 就是这个功能。如果由perf_counter 库自己来初始化SysTick,它会为了自己功能更可靠将 SysTick的溢出值(LOAD寄存器)设置为最大值(0x00FFFFFF)。
void main(void){ system_clock_update(); //! 更新CPU工作频率 SystemCoreClock = 72000000ul //! 假设更新后的系统频率是 72MHz init_cycle_counter(true); ...} 当然,不要忘记向已经存在的SysTick_Handler()内加入perf_counter()的插入函数:
__attribute__((used)) //!< 避免下面的处理程序被编译器优化掉void SysTick_Handler(void){ ... //! 这个函数来自于 perf_counter.h user_code_insert_to_systick_handler(); ...} 至此,我们就完成了 perf_counter 模块在 GCC和IAR中的部署。
CPU资源占用(百分比) = (函数运行所需的时间)➗ (算法运行间隔的最小值) ✖️ 100%
对于【函数运行所需的时间】和【算法运行间隔的最小值】来说,虽然它们都是时间单位,但考虑到CPU的频率是给定的(不变的),因此,这里的时间单位在乘以CPU的工作频率后都可以被换算为CPU的周期数。举例来说,假如【算法运行间隔的最小值】是 20ms、CPU的频率是72MHz,那么对应的周期数就是 72000000 * (20ms / 1000ms) = 1440000 个周期。看来上述公式中唯一需要我们实际测量的就是【函数运行所需的周期数】了。
perf_counter 提供了一个非常简单的运算符:__cycleof__()。假设我们要测量的代码片断如下:
带入上述公式:525139 / 14400000 * 100% ≈ 36.5%
就计算出这个算法占用了大约 36.5% 的CPU资源,值得说明的是,从原理上看,这一方式对裸机和RTOS同样有效哦。
有的小伙伴很快会说,我的系统并不允许我调用printf,那我还可以使用 __cycleof__() 么?当然了!就继续以上述代码为例子:
...__cycleof__("my algorithm", { nCycleUsed = _; }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c();}... 这里的代码所实现的功能是:
-
测量了用户函数 my_algorithm_step_xxx() 所使用的周期数:
-
测量的结果被转存到了一个叫做 nCycleUsed 的变量中;
-
__cycleof__() 将不会调用 printf() 进行任何内容输出。
我相信很多小伙伴会揉了揉眼睛、仔细看了又看,然后回过头来满头问号:
这是C语言?
这是什么语法?
不要怀疑,这就是C语言,只不过使用了一点GCC的语法扩展(感兴趣的小伙伴可以复制这里的连接 https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html#Statement-Exprs),考虑到本文只介绍 perf_counter 如何使用,而对其如何实现的并不关心,我们不妨略过GCC扩展语法的部分,专门来看看上述代码的使用细节:
-
首先,为了方便大家观察,我们先忽略圆括号内的部分:
-
接下来,我们专门来看__cycleof__() 圆括号中的部分:
...__cycleof__("my algorithm", { nCycleUsed = _; }){...}... 容易发现,如果以“,” 为分隔符,那么实际传递给 __cycleof__() 的是两个部分:
1、标注测量名称的字符串
这里,对于表示测量名称的字符串"my algorithm",在这一用法下在最终的编译结果里并不会占用任何RAM或者是ROM,但作为语法结构是必须的。
对于花括号所囊括的代码片段来说,实际上在这个花括号里,你几乎可以为所欲为:
-
你可以写任意数量的代码
-
你可以调用函数
-
你可以定义变量(当然这里定义变量肯定就是局部变量了)
...__cycleof__("my algorithm", { nCycleUsed = _; }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c();}
printf("Cycle Used %d", _);
编译器会毫不客气的告诉你 "_" 是一个未定义的变量,反之如果你这么做:
...__cycleof__("my algorithm", { nCycleUsed = _; printf("Cycle Used %d", _); }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c();} 则会看到你心怡的输出结果:
...do { int64_t _ = get_system_ticks(); { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c(); } _ = get_system_ticks() - _; //! 我们添加的代码 nCycleUsed = _; printf("Cycle Used %d", _);} while(0); 是不是突然就没有那么神秘了?通过“逻辑等效”的形式展开,我们很容易发现一些有趣的内容:
-
起核心作用的是一个叫做 get_system_ticks() 的函数。实际上它返回的是从复位后 SysTick被使能至今所经历的 CPU 周期数——由于它是int64_t 的类型,因此不用担心超过 SysTick 24位计数器的量程,也不用担心人类历史范围内会发生溢出的可能。 知道这一点后,聪明的小伙伴就可以自己整活儿了。
-
由于 "_" 是一个局部变量,因此可以判断 __cycleof__() 是支持嵌套的。
void calib_perf_counter(void) { int64_t lTemp = get_system_tick(); s_lPerfCalib = get_system_tick() - lTemp;}
int64_t get_perf_counter_calib(void){ return s_lPerfCalib;} 具体如何使用,这里就不再赘述了。
【说在后面的话】perf_counter 仍然在不停的演化中,这多亏了开源社区不断的使用和反馈。perf_counter 的应用场景实际上非常广泛,包括但不限于:
-
为裸机或者RTOS提供Cycle级别的性能测量;
-
评估代码片段的CPU占用;
-
算法精细优化时用于测量和观察优化的效果;
-
测量中断的响应时间;
-
测量中断的发生间隔(查找最短时间间隔);
-
评估GUI的帧率或者刷新率;
-
与SystemCoreClock计算后,获得一个系统时间戳(Timestamp);
-
当做Realtime Clock的基准;
-
作为随机数种子
-
……
end