首页 > 其他分享 >浅谈单片机的gcc优化级别__以双音频信号发生器为例

浅谈单片机的gcc优化级别__以双音频信号发生器为例

时间:2024-11-09 15:46:41浏览次数:3  
标签:__ gcc 浅谈 buf auto fps tick 优化 O3

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

相关文章

  • 25-009、基于STM32单片机智能公交车自动报站系统RFID语音报站+液晶显示温度和烟雾值设
    本系统由STM32F103C8T6单片机核心板、2.4寸TFT彩屏、JR6001语音播报电路、ULN2003步进电机驱动电路、RC522-RFID刷卡识别电路、轻触开关检测电路、按键电路组成。【1】STM32单片机驱动2.4寸TFT彩屏实时显示所有站台信息、当前公交运行方向、当前到站的站点(并有图标显示);语音播......
  • 【论文阅读】您的 AI 生成的代码真的安全吗?Evaluating Large Language Models on Secu
    IsYourAI-GeneratedCodeReallySafe?EvaluatingLargeLanguageModelsonSecureCodeGenerationwithCodeSecEval标题:您的AI生成的代码真的安全吗?基于CodeSecEval对LLMs代码生成的安全性进行评估摘要:*【Background】大语言模型(LLMs)在代码生成和代码修复方面取......
  • 线程池创建方式
    线程池创建方式  一、方式一:通过ThreadPoolExecutor构造函数来创建(推荐)  方式二:通过Executor框架的工具类Executors来创建。  Executors工具类提供的创建线程池的方法如下图所示:  可以看出,通过Executors工具类可以创建多种类型的线程池,包括:  1. Fixed......
  • 【51单片机】程序实验1——点亮LED
    由于博主还未学习数字电路和计算机组成原理,因此本系列先开展单片机软件编程的内容,硬件结构的内容简单带过,会考虑安排在后续学习计划中,编程入门部分不会深入涉及单片机电路结构原理。博主已有C语言基础,因此相关内容不会从零开始赘述主要参考学习资料:B站【普中官方】51单片......
  • 计算机网络:网络安全(网络安全概述)_网络安全概论
    一、网络安全的概念与特征由于计算机网络多样的连接形式、不均匀的终端分布,以及网络的开放性和互联性等特征,使通过互联网传输的数据较易受到监听、截获和攻击。伴随着虚拟化、大数据和云计算技术等各种网络新技术广泛而深入的应用,如今网络安全问题已经和几乎所有传统的安全问......
  • 【MATLAB源码-第212期】基于matlab的8PSK的log-map软解调仿真,输出误码率曲线和星座图
    操作环境:MATLAB2022a1、算法描述1.8PSK调制和解调系统8PSK(8相位移键控)是一种数字调制技术,它通过改变载波信号的相位来传输数据。在这个系统中,我们将介绍8PSK的工作原理、调制过程、信道模型、解调过程,特别是log-MAP软解调的实现。1.18PSK调制的基本原理8PSK调制的基本......
  • 网络安全技术概论知识点
    目录第一章网络安全基础知识点例题第二章网络安全技术基础知识点第三章网络安全体系管理知识点例题第四章黑客攻防与检测防御知识点例题第五章、第六章第七章计算机及手机病毒防范例题第八章防火墙技术知识点第九章操作系统安全第十章数据库及数据安全知识点第......
  • 2024最全CTF入门指南、CTF夺旗赛及刷题网站(建议收藏!)
    CTF(CaptureTheFlag)中文译作:夺旗赛CTF起源于1996年DEFCON全球黑客大会,以代替之前黑客们通过互相发起真实攻击进行技术比拼的方式。发展至今,已经成为全球范围网络安全圈流行的竞赛形式,2013年全球举办了超过五十场国际性CTF赛事。而DEFCON作为CTF赛制的发源地,DEFCONCTF也成......
  • CTF 入门指南:从零开始学习网络安全竞赛
    一、引言在当今数字化的世界中,网络安全已经成为了至关重要的领域。而CTF(CaptureTheFlag,夺旗赛)作为网络安全竞赛的一种重要形式,为广大爱好者和学习者提供了一个绝佳的平台,让他们能够在实战中提升自己的技能和知识。如果您对网络安全充满热情,并且渴望在这个领域中一展身手,那......
  • 分支语句【if】 pk 【switch】
    一、if语句有三种形式:①if(表达式)   语句inta=5;if(a>0)puts("大于0");② if(表达式)    语句   else    语句  ​ inta=5; if(a>0) puts("大于0"); else puts("小于等于0");​③ if(表达式)        ......