如何“優(yōu)雅”的測(cè)量系統(tǒng)性能
時(shí)間:2021-09-22 14:56:24
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]【說在前面的話】在之前的文章《【嵌入式秘術(shù)】相約榨干SysTick的每一滴汁水》里,我們介紹了一個(gè)以“寄居”形式(也就是在不影響用戶已有SysTick應(yīng)用的情況下)測(cè)量CPU性能的開源函數(shù)庫(kù)perf_counter。其倉(cāng)庫(kù)連接如下:https://github.com/Gorgo...
【說在前面的話】
https://github.com/GorgonMeducer/perf_counter
不知不覺中,perf_counter已經(jīng)經(jīng)歷了大大小小7個(gè)版本:
-
提高了delay_us() 的精度
-
增加了對(duì)GCC、IAR的支持
-
改進(jìn)了 __cycleof__() 宏,使其支持嵌套、并不再?gòu)?qiáng)制綁定 printf()
如果你使用的是Arm Compiler5(armcc)或是Arm Compiler 6(armclang),移植就特別簡(jiǎn)單。你可以按照這篇文章的手把手教程在5分鐘內(nèi)完成部署。
【關(guān)于對(duì)GCC和IAR的支持】對(duì)于GCC和IAR來說,由于它們都不支持 Arm Compiler 5/6 所特有的 Linker語法——$Sub$$ 和 $Super$$,因此無法直接通過 Lib 的方式實(shí)現(xiàn)對(duì)已有SysTick應(yīng)用的 “寄居”——這里就只能忍痛割愛了。這并不影響我們以源代碼的形式將它們加入已有的 GCC 或是 IAR 工程。大體步驟如下:
第一步:將 perf_counter.c 和 perf_counter.h 拷貝到你的工程目錄下,并將perf_counter.c 加入到編譯列表中;
第二步:將 perf_counter.h 所在的路徑加入到編譯器的頭文件搜索路徑中;
第三步:perf_counter.c 依賴 CMSIS 5.4.0 及其以上版本,確保你的工程中正確的包含了對(duì)CMSIS的支持。(這里就不再贅述)。
__attribute__((used)) //!< 避免下面的處理程序被編譯器優(yōu)化掉void SysTick_Handler(void){ //! 這個(gè)函數(shù)來自于 perf_counter.h user_code_insert_to_systick_handler();}
然后我們?cè)?main() 函數(shù)里初始化 perf_counter 服務(wù):
void main(void){ system_clock_update(); //! 更新CPU工作頻率 SystemCoreClock = 72000000ul //! 假設(shè)更新后的系統(tǒng)頻率是 72MHz init_cycle_counter(false); ...} 需要特別注意的是:由于用戶并沒有自己初始化 SysTick,因此我們需要將這一情況告知 perf_counter 庫(kù)——由它來完成對(duì) SysTick 的初始化——這里傳遞 false 給函數(shù) init_cycle_counter() 就是這個(gè)功能。如果由perf_counter 庫(kù)自己來初始化SysTick,它會(huì)為了自己功能更可靠將 SysTick的溢出值(LOAD寄存器)設(shè)置為最大值(0x00FFFFFF)。
void main(void){ system_clock_update(); //! 更新CPU工作頻率 SystemCoreClock = 72000000ul //! 假設(shè)更新后的系統(tǒng)頻率是 72MHz init_cycle_counter(true); ...} 當(dāng)然,不要忘記向已經(jīng)存在的SysTick_Handler()內(nèi)加入perf_counter()的插入函數(shù):
__attribute__((used)) //!< 避免下面的處理程序被編譯器優(yōu)化掉void SysTick_Handler(void){ ... //! 這個(gè)函數(shù)來自于 perf_counter.h user_code_insert_to_systick_handler(); ...} 至此,我們就完成了 perf_counter 模塊在 GCC和IAR中的部署。
CPU資源占用(百分比) = (函數(shù)運(yùn)行所需的時(shí)間)? (算法運(yùn)行間隔的最小值) ?? 100%
對(duì)于【函數(shù)運(yùn)行所需的時(shí)間】和【算法運(yùn)行間隔的最小值】來說,雖然它們都是時(shí)間單位,但考慮到CPU的頻率是給定的(不變的),因此,這里的時(shí)間單位在乘以CPU的工作頻率后都可以被換算為CPU的周期數(shù)。舉例來說,假如【算法運(yùn)行間隔的最小值】是 20ms、CPU的頻率是72MHz,那么對(duì)應(yīng)的周期數(shù)就是 72000000 * (20ms / 1000ms) = 1440000 個(gè)周期??磥砩鲜龉街形ㄒ恍枰覀儗?shí)際測(cè)量的就是【函數(shù)運(yùn)行所需的周期數(shù)】了。
perf_counter 提供了一個(gè)非常簡(jiǎn)單的運(yùn)算符:__cycleof__()。假設(shè)我們要測(cè)量的代碼片斷如下:
帶入上述公式:525139 / 14400000 * 100% ≈ 36.5%
就計(jì)算出這個(gè)算法占用了大約 36.5% 的CPU資源,值得說明的是,從原理上看,這一方式對(duì)裸機(jī)和RTOS同樣有效哦。
有的小伙伴很快會(huì)說,我的系統(tǒng)并不允許我調(diào)用printf,那我還可以使用 __cycleof__() 么?當(dāng)然了!就繼續(xù)以上述代碼為例子:
...__cycleof__("my algorithm", { nCycleUsed = _; }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c();}... 這里的代碼所實(shí)現(xiàn)的功能是:
-
測(cè)量了用戶函數(shù) my_algorithm_step_xxx() 所使用的周期數(shù):
-
測(cè)量的結(jié)果被轉(zhuǎn)存到了一個(gè)叫做 nCycleUsed 的變量中;
-
__cycleof__() 將不會(huì)調(diào)用 printf() 進(jìn)行任何內(nèi)容輸出。
我相信很多小伙伴會(huì)揉了揉眼睛、仔細(xì)看了又看,然后回過頭來滿頭問號(hào):
這是C語言?
這是什么語法?
不要懷疑,這就是C語言,只不過使用了一點(diǎn)GCC的語法擴(kuò)展(感興趣的小伙伴可以復(fù)制這里的連接 https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html#Statement-Exprs),考慮到本文只介紹 perf_counter 如何使用,而對(duì)其如何實(shí)現(xiàn)的并不關(guān)心,我們不妨略過GCC擴(kuò)展語法的部分,專門來看看上述代碼的使用細(xì)節(jié):
-
首先,為了方便大家觀察,我們先忽略圓括號(hào)內(nèi)的部分:
-
接下來,我們專門來看__cycleof__() 圓括號(hào)中的部分:
...__cycleof__("my algorithm", { nCycleUsed = _; }){...}... 容易發(fā)現(xiàn),如果以“,” 為分隔符,那么實(shí)際傳遞給 __cycleof__() 的是兩個(gè)部分:
1、標(biāo)注測(cè)量名稱的字符串
這里,對(duì)于表示測(cè)量名稱的字符串"my algorithm",在這一用法下在最終的編譯結(jié)果里并不會(huì)占用任何RAM或者是ROM,但作為語法結(jié)構(gòu)是必須的。
對(duì)于花括號(hào)所囊括的代碼片段來說,實(shí)際上在這個(gè)花括號(hào)里,你幾乎可以為所欲為:
-
你可以寫任意數(shù)量的代碼
-
你可以調(diào)用函數(shù)
-
你可以定義變量(當(dāng)然這里定義變量肯定就是局部變量了)
...__cycleof__("my algorithm", { nCycleUsed = _; }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c();}
printf("Cycle Used %d", _);
編譯器會(huì)毫不客氣的告訴你 "_" 是一個(gè)未定義的變量,反之如果你這么做:
...__cycleof__("my algorithm", { nCycleUsed = _; printf("Cycle Used %d", _); }) { my_algorithm_step_a(); my_algorithm_step_b(); ... my_algorithm_step_c();} 則會(huì)看到你心怡的輸出結(jié)果:
...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); 是不是突然就沒有那么神秘了?通過“邏輯等效”的形式展開,我們很容易發(fā)現(xiàn)一些有趣的內(nèi)容:
-
起核心作用的是一個(gè)叫做 get_system_ticks() 的函數(shù)。實(shí)際上它返回的是從復(fù)位后 SysTick被使能至今所經(jīng)歷的 CPU 周期數(shù)——由于它是int64_t 的類型,因此不用擔(dān)心超過 SysTick 24位計(jì)數(shù)器的量程,也不用擔(dān)心人類歷史范圍內(nèi)會(huì)發(fā)生溢出的可能。 知道這一點(diǎn)后,聰明的小伙伴就可以自己整活兒了。
-
由于 "_" 是一個(gè)局部變量,因此可以判斷 __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 仍然在不停的演化中,這多虧了開源社區(qū)不斷的使用和反饋。perf_counter 的應(yīng)用場(chǎng)景實(shí)際上非常廣泛,包括但不限于:
-
為裸機(jī)或者RTOS提供Cycle級(jí)別的性能測(cè)量;
-
評(píng)估代碼片段的CPU占用;
-
算法精細(xì)優(yōu)化時(shí)用于測(cè)量和觀察優(yōu)化的效果;
-
測(cè)量中斷的響應(yīng)時(shí)間;
-
測(cè)量中斷的發(fā)生間隔(查找最短時(shí)間間隔);
-
評(píng)估GUI的幀率或者刷新率;
-
與SystemCoreClock計(jì)算后,獲得一個(gè)系統(tǒng)時(shí)間戳(Timestamp);
-
當(dāng)做Realtime Clock的基準(zhǔn);
-
作為隨機(jī)數(shù)種子
-
……
end