一、简介
gcc有多种优化级别,一般不选择的情况下,IDE默认是按照-Og或这-O2优化的。
以gcc编译器为例,浅谈一下优化级别,我们常见的优化一般是指gcc的-O2、-Og。除此之外,gcc还有-Os等一系列优化,链接器也有优化级别。
基于单片机的开发,如果关注的是生成代码的大小,那么可以考虑-Os和-Oz。如果在乎性能的话,可以尝试-O2以上的优化级别
-O优化
一共有级别。当然上面还有-O4甚至-O5.
- -O0:无优化,适合调试。
- -O1:基本优化,适合快速迭代开发。
- -O2:中级优化,适合大多数生产环境。
- -O3:高级优化,适合对性能要求较高的应用。
- -Ofast:极端优化,适合对性能要求极高且对标准合规性要求不高的应用。
- -Og:调试优化,适合开发和调试阶段。
- -Os:优化生成的代码大小,而不是性能。
- -Oz:极度优化大小,进一步优化生成的代码大小,比 -Os更激进,可能会牺牲一些性能。
- -flto:在链接阶段进行优化,允许进行跨文件的优化,进一步提高性能,但耗时间
- -fprofile-generate(生成配置文件) 和 -fprofile-use(基于配置文件的优化)
从下面生成的代码体积来看优化对程序的影响
【-O0】
【-O1】
【-O2】
【-O3】
【-Ofast】
【-Og】
【-Os】
【-Oz】
-flto
启用链接时优化,它会跨文件进行优化,会把所有东西混杂起来再优化,同时也会影响调试信息的生成。这就意味着,前面的-O级优化还能把编译出的二进制文件与你的源码对应起来,现在只能与反汇编对应。
如下图,你甚至能看到CPU的那12个寄存器,此时展现在调试窗口的就是反汇编了。如果你想要让IDE调试程序时与源码对应,那么就需要加上-g编译标志
做个对比,虽然仅从代码量来观察是片面了许多,但多少能反映一些(原先代码忘记做速度测试了)。
上图是未开-flto,下图是开了-flto
【-O1】
【-O2】
【-O3】
【-Ofast】
【-Os】
【-Oz】
只不过有时候同样一份代码,用不同的方式优化可能还会报错,比如下面是-Og -flto优化,因为链接库的某种不知名原因
上述的-O级优化其实是由一系列单项优化组成的,可以组合,更适合竞赛宝宝体质的#pragram
-fwhole-program
目标:在整个程序范围内进行优化。
特点:
允许编译器在链接阶段对整个程序进行全局优化。
通常与 -O3 一起使用,以获得更高的性能。
-fprofile-generate 和 -fprofile-use
目标:基于运行时数据进行优化。
特点:
-fprofile-generate:生成配置文件。
-fprofile-use:使用生成的配置文件进行优化。
可以显著提高性能,特别是在热点路径上。
-fipa-cp-algorithm
目标:改进跨过程常量传播算法。
特点:
用于改进跨过程的常量传播,提高代码性能。
-fipa-pta
目标:改进指针分析。
特点:
用于改进指针分析,提高代码性能。
-funroll-loops
目标:展开循环。
特点:通过展开循环减少循环开销,提高性能。
-finline-functions
目标:内联函数。
特点:自动内联小函数,减少函数调用开销。
-fomit-frame-pointer
目标:省略帧指针。
特点:在函数调用中省略帧指针,减少寄存器使用,提高性能。
-fstrict-aliasing
目标:启用严格的别名规则。
特点:允许编译器进行更激进的优化,假设不同类型的指针不会指向同一内存地址。
-ftree-vectorize
目标:启用向量化优化。
特点:将循环中的标量操作转换为向量操作,利用 SIMD 指令集提高性能。
-floop-interchange
目标:交换循环顺序。
特点:优化嵌套循环的顺序,提高缓存利用率。
-floop-strip-mine
目标:分割循环。
特点:将大循环分割成多个小循环,提高缓存利用率。
-floop-block
目标:块划分循环。
特点:将循环体划分为多个块,提高缓存利用率。
-fgraphite
目标:启用 Graphite 循环变换框架。
特点:使用高级循环变换技术优化循环性能。
-fipa-sra
目标:启用跨过程结构体拆解。
特点:在跨过程调用中拆解结构体,减少内存访问开销。
链接器也有一系列优化,就是不常用,包括上面提到的一系列组合,对于单片机开发来说
-Wl,--hash-style=both 目标:使用两种哈希风格。 特点:链接器使用两种哈希风格(SYSV 和 GNU),提高符号查找效率。 -Wl,--no-undefined 目标:禁止未定义的符号。 特点:链接器在链接时检查未定义的符号,确保所有符号都已定义。 -Wl,--no-merge-exidx-entries 目标:禁止合并异常索引条目。 特点:防止链接器合并异常索引条目,确保异常处理的准确性。 -Wl,--sort-common 目标:按大小排序公共符号。 特点:链接器按大小排序公共符号,提高内存布局的效率。 -Wl,--sort-section=name 目标:按名称排序节区。 特点:链接器按名称排序节区,提高内存布局的效率。 -Wl,--no-keep-memory 目标:释放内存。 特点:链接器在链接过程中释放不再需要的内存,减少内存占用。
二、测试
这里做了一点点简单不那么严谨的小测试,使用的测试工程为下面链接中的双音频信号发生器ichliebedich-DaCapo/STM32F407VET6: stm32f407vet6 (github.com)
-O0:
结果很感人,烧录时一切正常
但按下按键后还没怎么执行就卡住了。
在卡住之后,我们停下来可以清楚地看到堆栈爆了(栈区溢出,下方蓝色的msp寄存区),直接的影响就是LVGL处理事件时,访问数组直接越界。换句话说,如果下次碰到了LVGL数组越界,那么就要怀疑是栈区溢出了。
现在看一看编译大小
-gc-sections是去除不用的段,--print-memory-usage是打印内存分布,Map=${BIN_DIR}/${PROJECT_NAME}.map是生成map映射文件。当然,前面都得有-Wl
add_link_options(-Wl,-gc-sections,--print-memory-usage,-Map=${BIN_DIR}/${PROJECT_NAME}.map)
-O1:
同样的代码,使用-O1可以很明显地看到优化情况
下面将以以内置的FPS组件显示,在128个数据点、线性插值算法、800Hz(只是虚拟的,不是真的)下进行测试
FPS组件代码是基于LVGL的文本框组件写的一个小类,没有做什么性能上的优化,但简单测试衡量一下性能变化还是可以做到的。
/*** @brief 工具类*/ class Tools { public:static inline auto fps_init(Font font, Coord x = 0, Coord y = 40, Coord width = 60, Coord height = 20) -> void;// 显示fpsstatic inline auto fps(bool time = true) -> void;static inline auto restart_fps() -> void;static inline auto set_right() -> void;static inline auto set_left() -> void;static inline auto set_center() -> void;static inline auto clear_fps() -> void;private:// 获取时间static inline auto get_tick() -> uint32_t;// 单线程更新事件static inline auto update_tick() -> void;private:static inline Obj_t label_fps{};static inline uint32_t count = 0;static inline uint32_t tick = 0; };/*** @brief fps功能初始化* @param font 指定字库中要有fps和十位数字,字体大小为13即可* @param x x轴* @param y y轴* @note 默认文本框为60*20,即宽60,高20,且文本为左对齐。*/ auto Tools::fps_init(Font font, Coord x, Coord y, Coord width, Coord height) -> void {Text label;label.init_font(font); #if SIMPLE_FPSlabel.init(label_fps, x, y, width, height, ""); #elselabel.init(label_fps, x, y, 60, 80, "fps\n0"); #endif}/*** @brief fps显示* @note 启用该功能之前必须先调用fps_init进行必要的初始化。启用fps显示,即在需要的地方调用本函数* 默认显示一帧需要的时间单位为ms*/ auto Tools::fps(bool time) -> void { #if SIMPLE_FPSchar buf[7];if (time){// 显示一帧的时间sprintf(buf, "%.2fms", 1.0 * get_tick() / (count++));} else{// 显示帧率sprintf(buf, "%.2f", 1000.0 * (count++) / get_tick());}Text::set_text(buf, label_fps); #elsechar buf[9];// 显示一帧的时间sprintf(buf, "fps\n%.2f", 1.0*get_tick() / (count++));// 显示帧率 // sprintf(buf, "fps\n%.2f", 1000.0*(count++))/get_tick();Text::set_text(buf, label_fps); #endif }auto Tools::get_tick() -> uint32_t {uint32_t temp_tick = lv_tick_get();// 防止溢出if (temp_tick < tick)temp_tick += (0xFFFF'FFFF - tick);elsetemp_tick -= tick;return temp_tick; }auto Tools::update_tick() -> void {tick = lv_tick_get(); }/*** @brief 重启fps*/ auto Tools::restart_fps() -> void {update_tick();count = 0; }auto Tools::set_right() -> void {Text::set_text_align(LV_TEXT_ALIGN_RIGHT, label_fps); }auto Tools::set_center() -> void {Text::set_text_align(LV_TEXT_ALIGN_CENTER, label_fps); }auto Tools::set_left() -> void {Text::set_text_align(LV_TEXT_ALIGN_LEFT, label_fps); }auto Tools::clear_fps() -> void {Text::set_text("", label_fps);}
可以看出,一帧所用时间为36.88ms左右,并且屏幕右侧有严重的漏墨现象,这是由于绘制像素点函数LCD_Set_Pixel不够卖力(主频不够高)导致的
-O2:
接下来使用-O2级别,同上面相比,我们可以看到RAM和Flash都略微增加了少许
接下来测试一下性能,从右上角的36.79ms可以看出,相比-O1可能有了那么一点点提升(因为不能排除误差),从观察效果来看,漏墨现象也是挺严重的。在我印象中,应该比-O1强一些才对,可能是这次没发挥好(不同工程、相同的优化级别,显现的效果是不同的)
-Og:
接下来我们看看平时最常用的调试级别优化能拿出怎样的成绩吧。首先是代码体积比-O1还小了一点,内存占用相同。
接下来看看性能,37.16ms,可能是由于调试信息的原因性能就略逊一筹,不过与-O1、-O2也大差不差
-O3:
接下来有请-O3大佬, 一出手就是非同凡响,RAM占用些许提升,ROM大幅提升
从性能上来看,竟然与前面差不多,那么可以说明一个问题,现在性能的瓶颈不在于算法,而在于打点速度。真是失策,测了这么多有种白费的感觉。
----------------
不过接下来换种算法测试一下,就以样条算法为例,这个与贝塞尔曲线差不多的速度,比线性插值慢,但稳定多了,帧率最后会趋于一个稳定的值,所以测试结果相对要可靠一些。
由于工程不变,所以就不继续展示代码大小了
-O1
-O2
-Og
-O3
-Ofast
代码体积比-O3多了一点点,-O3 -ffast-math、-O4、-O5在代码体积上与-O3完全一致
该优化被clang淘汰掉了,取而代之的是-O3 -ffast-math。gcc还有是-Ofast的
-Os
代码确实小了一些
看看性能,63.63ms与-Og差不多
-Oz
看来代码的体积已经被压缩到极致了
性能与-Os差不多,但与-Ofast比起来就相对明显
至于-flto优化这个我现在无法测试,因为改了文件组织编译方式,把大部分文件都分别编译为静态库,然后再统一链接成elf文件。所以无法使用-flto,一使用就会出现找不到定义的错误。之前没改CMakelists前,使用-flto,代码体积上确有优化,但性能没有测试过。
add_compile_options(-flto )
-O3及以上的优化要慎重对待,上次基于样条算法编写一个模板函数,只有在-O2下可以正常运行,开-O3以上 会卡死,开-O2以下堆栈会爆,真是让人摸不到头脑。后来也不知改了什么,或许是改动了其他函数间接导致这个模板函数又行了,在-O1到-Ofast均可正常使用。
编译器优化,很玄
另提一嘴,在使用总大小相同的缓冲数组情况下,LVGL的双缓冲要优于单缓冲。设置双缓冲也很简单,只有在旁边另加一个静态数组,然后把数组名填入到lv_disp_draw_buf_init的第三个参数中
/*** @brief 初始化显示驱动* @tparam flush 涂色函数,有LCD驱动提供* @note 为了让lambda表达式可以不用捕获外部函数,只能使用函数模板。如果使用函数指针来传递就必须要显示捕获*/ template<void (*flush)(uint16_t, uint16_t, uint16_t, uint16_t, const uint16_t *)> auto GUI::disp_drv_init() -> void {// 在缓冲数组总大小同等的情况下,双缓冲明显优于单缓冲static lv_disp_draw_buf_t draw_buf_dsc;static lv_color_t buf_2_1[MY_DISP_HOR_RES * MY_DISP_BUF_SIZE];static lv_color_t buf_2_2[MY_DISP_HOR_RES * MY_DISP_BUF_SIZE];lv_disp_draw_buf_init(&draw_buf_dsc, buf_2_1, buf_2_2,MY_DISP_HOR_RES * MY_DISP_BUF_SIZE); /*Initialize the display buffer*/lv_disp_drv_init(&disp_drv); /*Basic initialization*/disp_drv.hor_res = MY_DISP_HOR_RES;disp_drv.ver_res = MY_DISP_VER_RES; // C环境下就不要使用lambda表达式,自行定义flush函数disp_drv.flush_cb = [](lv_disp_drv_t *, const lv_area_t *area, lv_color_t *color_p){flush(area->x1, area->y1, area->x2, area->y2, (const uint16_t *) color_p);};disp_drv.draw_buf = &draw_buf_dsc;lv_disp_drv_register(&disp_drv); }