IDE: CLion
HOST: Windows 11
MinGW:x86_64-14.2.0-release-posix-seh-ucrt-rt_v12-rev0
GCC: arm-gnu-toolchain-13.3.rel1-mingw-w64-i686-arm-none-eabi
一、简介
gcc有多种优化级别,一般不选择的情况下,IDE默认是按照-Og或这-O2优化的。
以gcc编译器为例,浅谈一下优化级别,我们常见的优化一般是指gcc的-O2、-Og。除此之外,gcc还有-Os等一系列优化,链接器也有优化级别。
基于单片机的开发,如果关注的是生成代码的大小,那么可以考虑-Os和-Oz。如果在乎性能的话,可以尝试-O2以上的优化级别
-O优化
一般有下面几个级别。当然上面还有-O4甚至-O5,不过个人感觉和-O3差不多.
- -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; // 显示fps static 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_FPS label.init(label_fps, x, y, width, height, ""); #else label.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_FPS char 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); #else char 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); else temp_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); }
————链接优化————
补充:
改了一下CMakelists组织目录,接下来可以使用-flto优化,不过可能是由于资源限制或者哪里忘了配置,我使用这个优化选项,只能串行编译 9 个 LTRANS 任务,无法使用并行,所以链接时会比较慢。以此工程为例,编译用了6.32s左右,链接用了7.68s左右。
下面将重复上面条件,在编译优化的基础上继续测试链接优化
-O0
在不开编译优化的情况下,链接优化会使代码膨胀
作为对比,下面是没有启用链接优化前
-O1
上图是链接优化后,下图是链接优化前,可以看到RAM略微上升,ROM小幅下降
性能为63.20ms,相较与编译优化的63.32ms,还是能看到些许优化效果的
-O2
上图为链接优化,相比与下图,内存占用略微减少,ROM小幅提升
一帧所用为62.98ms,相较于先前的63.04ms,差不多
-Og
内存分布上来看,RAM和FLASH都下降了
相较于先前的63.63ms,略微提升
-O3
相较于先前,ROM涨幅比较大,内存略微下降
相较于先前的63.03ms,此时的62.95ms略微提升
-Ofast
ROM小幅增加,RAM略微下降
可以看到性能上与前面的-O3也差不多
-Os
此时可以看到RAM略微下降,ROM小幅下降
那么看看性能如何呢?结果很amazing啊,性能虽然连-O1都比不了,与没开链接优化前的63.63ms也差不多,但屏幕干净了许多,不怎么漏墨
-Oz
虽比先前的占用略微减小,但与链接优化的-Os差不多
与上面的-Os差不多
三、结论
没有优化绘制算法前,不同优化级别差距还是比较明显的
在经历了一系列手动优化后,我们可以看到除了-O0外,编译器(或连接器)优化对整体程序的性能优化并不怎么理想,对存储占用倒还算比较有用。换句话说,由于单片机上的计算资源一般并不怎么强,当软件手动优化到一定级别后,软件对整体性能的影响就不怎么大了,硬件的性能瓶颈对整体性能的制约占主要地位。
事实上,以本次为例,开启优化仅能把性能从188ms提升到174ms。但如果优化算法、增加缓冲数组、开启硬件优化(使用DMA)等,却可以从174ms提升到前面的64ms左右。
基于此,平时可以开-O3优化,不过要注意-O3下的优化很容易把一些变量优化掉,比如你随便定义了一个全局变量keyflag,然后在while循环里判断,那么很有可能会把它优化为寄存器,就导致即使接收到了按键信号,但寄存器也没有及时改变,进而无法正确及时处理按键任务。
工程基本完善后,那么可以尝试-Oz,如果与-O3性能差不多,那么可以考虑使用-Oz,这样会进一步降低存储占用。工程确认无误后,可以开-flto进行最后的优化,进一步减少存储占用,并提高些许性能。
标签:__,gcc,浅谈,buf,auto,fps,tick,优化,O3 From: https://blog.csdn.net/m0_74349248/article/details/143092521