Keil5,ARM编译器 软件优化注意事项

这篇具有很好参考价值的文章主要介绍了Keil5,ARM编译器 软件优化注意事项。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

优化C代码中的环路终止

循环是大多数程序中的常见结构。由于大量的执行时间通常花费在循环中,因此值得关注时间关键循环。

如果不谨慎地编写,环路终止条件可能会导致大量开销。在可能的情况下:

  • 使用简单的终止条件。

  • 写入倒计时到零循环。

  • 使用 unsigned int 类型的计数器。

  • 测试与零的相等性。

单独或组合遵循这些准则中的任何或全部准则可能会产生更好的代码。

下表显示了用于计算 n! 的例程的两个示例实现,它们共同说明了环路终止开销。第一个实现使用递增循环计算 n!,而第二个例程使用递减循环计算 n!

表7-1 递增和递减循环的C代码

递增循环 递减循环
int fact1(int n)
{
    int i, fact = 1;
    for (i = 1; i <= n; i++)
        fact *= i;
    return (fact);
}
int fact2(int n)
{
    unsigned int i, fact = 1;
    for (i = n; i != 0; i--)
        fact *= i;
    return (fact);
}

下表显示了 armclang -Os -S --target=armv8a-arm-none-eabi 针对上述每个示例实现生成的机器代码的相应反汇编。

表 7-2 C 递增和递减循环的反汇编

递增循环 递减循环
fact1:                                  
        mov     r1, r0
        mov     r0, #1
        cmp     r1, #1
        bxlt    lr
        mov     r2, #0
.LBB0_1:                                
        add     r2, r2, #1
        mul     r0, r0, r2
        cmp     r1, r2
        bne     .LBB0_1
        bx      lr
fact2:                                  
        mov     r1, r0
        mov     r0, #1
        cmp     r1, #0
        bxeq    lr
.LBB1_1:                                
        mul     r0, r0, r1
        subs    r1, r1, #1
        bne     .LBB1_1
        bx      lr

比较反汇编表明,递增循环反汇编中的 ADD 和 CMP 指令对已替换为递减循环反汇编中的单个 SUBS 指令。由于 SUBS 指令更新状态标志(包括 Z 标志),因此不需要显式 CMP r1、r2 指令。

除了在循环中保存指令外,变量 n 不必在循环的生命周期内可用,从而减少了必须维护的寄存器数量。这简化了寄存器分配。如果原始终止条件涉及函数调用,则更为重要。例如:

for (...; i < get_limit(); ...);

将循环计数器初始化为所需迭代次数,然后递减到零的技术也适用于 while 和 do 语句。

 

C 代码中的循环展开

循环是大多数程序中的常见结构。由于大量的执行时间通常花费在循环中,因此值得关注时间关键循环。

可以展开小循环以获得更高的性能,但缺点是代码大小增加。展开循环时,循环计数器需要更新的频率较低,执行的分支也较少。如果循环只迭代几次,则可以完全展开,使循环开销完全消失。编译器在 -O3 -Otime 处自动展开循环。否则,任何展开都必须在源代码中完成。

注意

手动展开循环可能会阻碍编译器自动重新滚动循环和其他循环优化。

可以使用下表中所示的两个示例例程来说明循环展开的优缺点。这两个例程都通过提取最低位并对其进行计数来有效地测试单个位,然后将该位移出。

第一种实现使用循环来计算位数。第二个例程是第一个展开四次的实现,通过将 n 的四个班次合并为一个班次来应用优化。

频繁展开提供了新的优化机会。

表 7-3 滚动和展开位计数循环的 C 代码

位计数循环 展开的位计数循环
int countbit1(unsigned int n)
{
    int bits = 0;
    while (n != 0)
    {
        if (n & 1) bits++;
        n >>= 1;
    }
    return bits;
}
int countbit2(unsigned int n)
{
    int bits = 0;
    while (n != 0)
    {
        if (n & 1) bits++;
        if (n & 2) bits++;
        if (n & 4) bits++;
        if (n & 8) bits++;
        n >>= 4;
    }
    return bits;
}

下表显示了编译器为上述每个示例实现生成的机器代码的相应反汇编,其中每个实现的 C 代码已使用 armclang -Os -S --target=armv8a-arm-none-eabi 编译。

表7-4 滚动和展开的位计数循环的反汇编

位计数循环 展开的位计数循环
countbit1:                              
        mov     r1, r0
        mov     r0, #0
        cmp     r1, #0
        bxeq    lr
        mov     r2, #0
.LBB0_1:                                
        and     r3, r1, #1
        cmp     r2, r1, lsr #1
        add     r0, r0, r3
        lsr     r3, r1, #1
        mov     r1, r3
        bne     .LBB0_1
        bx      lr
countbit2:                              
        mov     r1, r0
        mov     r0, #0
        cmp     r1, #0
        bxeq    lr
        mov     r2, #0
.LBB1_1:                                
        and     r3, r1, #1
        cmp     r2, r1, lsr #4
        add     r0, r0, r3
        ubfx    r3, r1, #1, #1
        add     r0, r0, r3
        ubfx    r3, r1, #2, #1
        add     r0, r0, r3
        ubfx    r3, r1, #3, #1
        add     r0, r0, r3
        lsr     r3, r1, #4
        mov     r1, r3
        bne     .LBB1_1
        bx      lr

位计数循环的展开版本比原始版本更快,但代码大小更大。

 

编译器优化和 volatile 关键字

较高的优化级别可以揭示某些程序中的问题,这些问题在较低的优化级别下并不明显,例如,缺少易失性限定符。

这可以通过多种方式表现出来。轮询硬件时,代码可能会卡在循环中,多线程代码可能会表现出奇怪的行为,或者优化可能会导致删除实现故意计时延迟的代码。在这种情况下,可能需要将某些变量声明为可变变量。

将变量声明为 volatile 告诉编译器,该变量可以在实现外部随时修改,例如,由操作系统、另一个执行线程(如中断例程或信号处理程序)或硬件进行修改。由于可变限定变量的值可以随时更改,因此每当在代码中引用该变量时,都必须始终访问内存中的实际变量。这意味着编译器无法对变量执行优化,例如,将其值缓存在寄存器中以避免内存访问。同样,在实现睡眠或计时器延迟的上下文中使用时,将变量声明为可变变量会告诉编译器有特定类型的行为是有意的,并且此类代码不得以删除预期功能的方式进行优化。

相反,当变量未声明为可变变量时,编译器可以假定其值不能以意外方式修改。因此,编译器可以对变量执行优化。

下表中的两个示例例程说明了 volatile 关键字的用法。这两个例程都在循环中读取缓冲区,直到状态标志 buffer_full 设置为 true。buffer_full的状态可以随程序流异步更改。

例程的两个版本仅在声明buffer_full的方式上有所不同。第一个例程版本不正确。请注意,变量 buffer_full 在此版本中未限定为 volatile。相比之下,例程的第二个版本显示了相同的循环,其中buffer_full被正确地限定为易失性

表 7-5 非易失性和易失性缓冲器环路的 C 代码

缓冲环路的非易失性版本 缓冲区循环的易失性版本
int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}
volatile int buffer_full;
int read_stream(void)
{
    int count = 0;
    while (!buffer_full)
    {
        count++;
    }
    return count;
}

下表显示了编译器为上述每个示例生成的机器代码的相应反汇编,其中每个实现的 C 代码已使用 armclang -Os -S --target=armv8a-arm-none-eabi 进行编译。

表7-6 非易失性和易失性缓冲器环路的反汇编

缓冲环路的非易失性版本 缓冲区循环的易失性版本
read_stream:                            
        movw    r0, :lower16:buffer_full
        movt    r0, :upper16:buffer_full
        ldr     r1, [r0]
        mvn     r0, #0
.LBB0_1:                                
        add     r0, r0, #1
        cmp     r1, #0
        beq     .LBB0_1     ; infinite loop
        bx      lr
read_stream:                            
        movw    r1, :lower16:buffer_full
        mvn     r0, #0
        movt    r1, :upper16:buffer_full
.LBB1_1:                                
        ldr     r2, [r1]     ; buffer_full
        add     r0, r0, #1
        cmp     r2, #0
        beq     .LBB1_1
        bx      lr

在上表中缓冲环路的非易失性版本的反汇编中,语句 LDR r1 [r0] 将 buffer_full 的值加载到寄存器 r1 外部标记为 .LBB0_1。由于 buffer_full 未声明为易失性,因此编译器假定其值不能在程序外部修改。编译器已将 buffer_full 的值读入 r0 中,因此在启用优化时会省略重新加载变量,因为其值无法更改。结果是标记为 的无限循环。LBB0_1

相反,在反汇编缓冲区循环的易失性版本时,编译器假定 buffer_full 的值可以在程序外部更改,并且不执行任何优化。因此,buffer_full 的值被加载到寄存器 r2 中,该寄存器位于标记为 的循环中。LBB1_1。因此,循环 .LBB1_1在汇编代码中正确实现。

为了避免由实现外部的程序状态更改引起的优化问题,每当变量的值可能以实现未知的方式意外更改时,就必须将变量声明为可变变量。

在实践中,每当出现以下情况时,都必须将变量声明为可变变量:

  • 访问内存映射的外围设备。

  • 在多个线程之间共享全局变量。

  • 访问中断例程或信号处理程序中的全局变量。

编译器不会优化已声明为可变变量的变量。

 

C 和 C++ 中的堆栈使用

C 和 C++ 都大量使用堆栈。

例如,堆栈包含:

  • 函数的返回地址。

  • 必须保留的寄存器,由 ARM 64 位架构 (AAPCS64) 的 ARM 体系结构过程调用标准确定,例如,在进入子例程时保存寄存器内容时。

  • 局部变量,包括局部数组、结构、联合,在 C++ 中还包括类。

有些堆栈使用并不明显,例如:

  • 如果局部整数或浮点变量溢出(即未分配给寄存器),则会为其分配堆栈内存。

  • 结构通常分配给堆栈。堆栈上保留了一个等效于 sizeof(struct) 的空间,该空间填充为 16 个字节的倍数。编译器尝试将结构分配给寄存器。

  • 如果在编译时已知数组大小的大小,则编译器会在堆栈上分配内存。同样,在堆栈上保留了一个等效于 sizeof(struct) 的空间,该空间填充为 16 个字节的倍数。

    注意

    可变长度数组的内存在运行时在堆上分配。
  • 一些优化可以引入新的临时变量来保存中间结果。优化包括:CSE 消除、实时范围拆分和结构拆分。编译器尝试将这些临时变量分配给寄存器。如果没有,它会将它们溢出到堆栈中。

  • 通常,为仅支持 16 位编码的 Thumb 指令的处理器编译的代码比 A64 代码、ARM 代码和为支持 32 位编码的 Thumb 指令的处理器编译的代码更多地使用堆栈。这是因为 16 位编码的 Thumb 指令只有 8 个寄存器可供分配,而 ARM 代码和 32 位编码的 Thumb 指令则有 14 个寄存器。

  • AAPCS64要求通过堆栈而不是寄存器传递某些函数参数,具体取决于它们的类型、大小和顺序。

估算堆栈使用情况的方法

堆栈使用情况很难估计,因为它依赖于代码,并且根据程序在执行时采用的代码路径,在运行之间可能会有所不同。但是,可以使用以下方法手动估计堆栈利用率的程度:

  • 使用 --callgraph 链接以生成静态调用图。这显示了有关所有功能的信息,包括堆栈使用情况。

    这将使用 .debug_frame 部分中的 DWARF 帧信息。使用 -g 选项进行编译以生成必要的 DWARF 信息。

  • 使用 --info=stack 或 --info=summarystack 链接以列出所有全局符号的堆栈使用情况。

  • 使用调试器在堆栈中的最后一个可用位置设置观察点,并查看是否命中了观察点。

  • 使用调试器,然后:

    1. 在内存中为比预期需要的堆栈大得多的堆栈分配空间。

    2. 用已知值的副本填充堆栈空间,例如 0xDEADDEAD

    3. 运行应用程序或应用程序的固定部分。目标是在测试运行中使用尽可能多的堆栈空间。例如,尝试执行最深嵌套的函数调用和静态分析找到的最坏情况路径。尝试在适当的位置生成中断,以便将它们包含在堆栈跟踪中。

    4. 应用程序完成执行后,检查内存的堆栈空间,查看有多少已知值已被覆盖。该空间在已使用部分中有垃圾,其余部分有已知值。

    5. 计算垃圾值的数量,然后乘以 sizeof(value),以给出它们的大小(以字节为单位)。

    计算结果显示了堆栈大小是如何增长的(以字节为单位)。

  • 使用固定虚拟平台 (FVP),并使用映射文件定义一个内存区域,不允许在内存中堆栈的正下方进行访问。如果堆栈溢出到禁止区域,则会发生数据中止,调试器可能会捕获数据中止。

减少堆栈使用的方法

通常,可以通过以下方式降低程序的堆栈要求:

  • 编写只需要少量变量的小函数。

  • 避免使用大型局部结构或数组。

  • 例如,通过使用替代算法来避免递归。

  • 最小化函数中每个点在任何给定时间使用的变量数。

  • 使用 C 块作用域并仅在需要的地方声明变量,因此与不同作用域使用的内存重叠。

C 块作用域的使用涉及仅在需要的地方声明变量。这通过重叠不同作用域所需的内存来最大程度地减少堆栈的使用。

 

最小化函数参数传递开销的方法

有多种方法可以最大程度地减少将参数传递给函数的开销。

例如:

  • 在 AArch64 状态下,可以有效地传递 8 个整数参数和 8 个浮点参数(总共 16 个)。在 AArch32 状态下,如果每个参数的大小不超过一个字,则确保函数采用四个或更少的参数。在 C++ 中,确保非静态成员函数采用的参数不超过一个参数,因为通常在 R0 中传递隐式 this 指针参数。
  • 如果函数需要超过参数的有效限制,请确保函数执行大量工作,以便超过传递堆叠参数的成本。
  • 将相关参数放在结构中,并在任何函数调用中传递指向该结构的指针。这减少了参数的数量并提高了可读性。
  • 对于 32 位体系结构,应尽量减少 long long 参数的数量,因为这些参数需要两个参数字,这两个参数字必须在偶数寄存器索引上对齐。
  • 对于 32 位体系结构,在使用软件浮点时,请尽量减少双精度参数的数量。

 文章来源地址https://www.toymoban.com/news/detail-779239.html

C 代码中的整数除以零错误

对于不支持 SDIV 除法指令的目标,可以使用相应的 C 库辅助函数 __aeabi_idiv0() 和 __rt_raise( 捕获和识别整数除以零错误

关于使用 __aeabi_idiv0() 捕获整数除以零错误

您可以使用 C 库辅助函数 __aeabi_idiv0() 捕获整数除以零错误,以便除以零返回一些标准结果,例如零。

整数除法是通过 C 库辅助函数 __aeabi_idiv() 和 __aeabi_uidiv() 在代码中实现的。这两个函数都检查除以零。

当检测到整数除以零时,将创建 __aeabi_idiv0() 的分支。因此,要将除法捕获为零,只需在 __aeabi_idiv0() 上放置一个断点。

该库提供了 __aeabi_idiv0() 的两种实现。默认值不执行任何操作,因此如果检测到除以零,则除法函数返回零。但是,如果使用信号处理,则会选择调用 __rt_raise(SIGFPE, DIVBYZERO) 的替代实现。

如果您提供自己的 __aeabi_idiv0() 版本,则除法函数将调用此函数。__aeabi_idiv0() 的函数原型为:

int __aeabi_idiv0(void);

如果 __aeabi_idiv0() 返回一个值,则该值用作除法函数返回的商。

关于使用 __rt_raise() 捕获整数除以零错误

默认情况下,整数除以零返回零。如果要截获除以零,可以重新实现 C 库辅助函数 __rt_raise()。

__rt_raise() 的函数原型为:

void __rt_raise(int signal, int type);

如果重新实现 __rt_raise(),则库会自动提供 __aeabi_idiv0() 的信号处理库版本,该版本调用 __rt_raise(),则该库版本的 __aeabi_idiv0() 将包含在最终映像中。

在这种情况下,当发生除以零错误时,__aeabi_idiv0() 调用 __rt_raise(SIGFPE, DIVBYZERO)。因此,如果重新实现 __rt_raise(),则必须选中 (signal == SIGFPE) & (type == DIVBYZERO) 以确定是否发生了除以零的情况。

识别 C 代码中的整数除以零错误

进入 __aeabi_idiv0() 时,链路寄存器 LR 包含应用程序代码中调用 __aeabi_uidiv() 除法例程后的指令地址。

通过在调试器中查找 LR 给出的地址处的 C 代码行,可以识别源代码中的违规行。

 

到了这里,关于Keil5,ARM编译器 软件优化注意事项的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • KEIL5MDK安装及V5编译器的安装

    目前keil官网最新版本的安装包都是默认安装V6编译器,不再自动安装V5编译器,而V5与V6编译器在编译的时候有很多代码不兼容,导致工程编译失败,所以我们使用新版keil5的时候,要用V5编译器就要自己安装V5编译器。 教程中所需的安装包(keil、破解工具、V5编译器)百度网盘

    2024年02月11日
    浏览(48)
  • KEIL MDK arm编译器 添加教程

    1.下载编译器安装文件 arm编译器6.16(适用于windows 64位)安装文件可在如下网址免费下载: arm编译器6.16(适用于Windows64位)-嵌入式文档类资源-CSDN文库 https://download.csdn.net/download/WG_IECAS/87342708 如需其他版本编译器,可到KEIL官网查找下载,快速网址: Arm Compiler downloads index h

    2023年04月08日
    浏览(58)
  • KEIL5MDK最新版(3.37)安装以及旧编译器(V5)安装

    方式一:keil5官网链接。需要填写信息(如左图,可以乱填),然后提交,点击软件下载(如右图),问题就是下载慢,但问题不大。 方式二:keil5云盘链接链接,提取码:1234,笔者当时最新的版本是3.37。 下载后安装,需要注意的是不要出现任何中文就行。 破解软件链接:

    2023年04月08日
    浏览(49)
  • proteus结合keil-arm编译器构建STM32单片机项目进行仿真

        proteus是可以直接创建设计图和源码的,但是源码编译它需要借助keil-arm编译器,也就是我们安装keil-mdk之后自带的编译器。     下面给出一个完整的示例,主要是做一个LED灯闪烁的效果。     新建工程指定路径,Schematic,PCB layout都选择默认,在最后创建项目工程向导的时

    2024年02月13日
    浏览(65)
  • ARM嵌入式编译器编译优化选项 -O

    Arm嵌入式编译器可以执行一些优化来减少代码量并提高应用程序的性能。不同的优化级别有不同的优化目标,不仅如此,针对某个目标进行优化会对其他目标产生影响。比如想减小生成的代码量,势必会影响到该代码的性能。所以优化级别总是这些不同目标(代码量,程序性

    2024年02月16日
    浏览(64)
  • 物联网|ARM|Keil同时安装Keil的C51、C251和MDK|增加V5编译器|物联网开发系列课程之零基础玩转Cortex-M系列CPU-学习笔记(1)

    1.物联网的定义 利用局部网络或互联网等通信技术把传感器、控制器、机器、人员和物等通过新的方式联在一起,形成人与物、物与物相联,实现信息化、远程管理控制和智能化的网络。 2.物联网的组成 3.物联网应用举例智能家居 1物联网的数据源头 2物联的局域网络源头 1

    2024年02月05日
    浏览(71)
  • Keil MDK安装armcc V5编译器

            不知道从什么时候开始,Keil MDK默认不支持V5的编译器了,里面默认只有V6的编译器,设置界面跟V5有很大的差异不太熟悉。最可怕的是,之前使用V5编译的工程,换成V6编译器后居然报错...虽然修改一下应该也可以正常编译,但,人总是习惯自己熟悉的东西。所以,

    2024年04月27日
    浏览(42)
  • ARM编译器5.06下载安装

    进入官方网站ARM Complier v5.06官网下载页面 进入后的界面为 往下翻,找到如图位置的5.06 for windows的文件,点击下载,下载时需要登录账号 先解压下载的压缩文件,在installer文件夹里面有一个 setup.exe 文件,双击它, 同意协议,在安装位置选择 keil 安装位置的 ARM 文件夹下,在

    2024年02月22日
    浏览(47)
  • arm系列交叉编译器各版本区别

    交叉编译器的命名规则:arch [-vendor] [-os] [-(gnu)eabi] [-language] arch - 体系架构, 如arm(ARM-32bit)、aarch64(ARM-64bit)、x86等; vendor -工具链提供商,经常省略,或用 none 替代; os - 目标操作系统, 如linux,没针对具体 os 则 用 none 替代。同时没有 vendor 和os 使用一个 none 替代。

    2024年02月01日
    浏览(61)
  • KEIL MDK5.37版本自行添加AC5(ARMCC)编译器

    从MDK5.37开始,AC5(ARMCC)编译器不再默认安装,需要独立安装。 下面是总结的安装步骤: 下载AC5(ARMCC)编译器: 1.官方页面(可能下载不成功) Legacy Arm Compilers – Arm Developer https://developer.arm.com/downloads/-/legacy-compilers 2.安富莱论坛(推荐,比较容易下载) 【安富莱】MDK5.29,5.30,

    2024年02月02日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包