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, r0mov     r0, #1cmp     r1, #1bxlt    lrmov     r2, #0
.LBB0_1:                                add     r2, r2, #1mul     r0, r0, r2cmp     r1, r2bne     .LBB0_1bx      lr
fact2:                                  mov     r1, r0mov     r0, #1cmp     r1, #0bxeq    lr
.LBB1_1:                                mul     r0, r0, r1subs    r1, r1, #1bne     .LBB1_1bx      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, r0mov     r0, #0cmp     r1, #0bxeq    lrmov     r2, #0
.LBB0_1:                                and     r3, r1, #1cmp     r2, r1, lsr #1add     r0, r0, r3lsr     r3, r1, #1mov     r1, r3bne     .LBB0_1bx      lr
countbit2:                              mov     r1, r0mov     r0, #0cmp     r1, #0bxeq    lrmov     r2, #0
.LBB1_1:                                and     r3, r1, #1cmp     r2, r1, lsr #4add     r0, r0, r3ubfx    r3, r1, #1, #1add     r0, r0, r3ubfx    r3, r1, #2, #1add     r0, r0, r3ubfx    r3, r1, #3, #1add     r0, r0, r3lsr     r3, r1, #4mov     r1, r3bne     .LBB1_1bx      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_fullmovt    r0, :upper16:buffer_fullldr     r1, [r0]mvn     r0, #0
.LBB0_1:                                add     r0, r0, #1cmp     r1, #0beq     .LBB0_1     ; infinite loopbx      lr
read_stream:                            movw    r1, :lower16:buffer_fullmvn     r0, #0movt    r1, :upper16:buffer_full
.LBB1_1:                                ldr     r2, [r1]     ; buffer_fulladd     r0, r0, #1cmp     r2, #0beq     .LBB1_1bx      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 位体系结构,在使用软件浮点时,请尽量减少双精度参数的数量。

 

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 代码行,可以识别源代码中的违规行。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/603551.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL三种常见存储引擎【理论】【需动手操作】

先放一个大佬的博客 等以后有时间按大佬写的 动手操作一下 链接 MySOL 的存储引擎是指 MySOL 数据库管理系统中用于处理数据存诸和检索的组件。 MySOL 常用的存储引擎有以下几个: InnoDB: InnoDB 是 MySQL(5.5)的默认存储引擎&#xff0c;支持事务处理、行级锁定和物理外键约…

2024年超详细的Python3学习路径规划

前言 基于Python3.5 1.第一阶段基础&#xff08;必须&#xff09; Python3 环境搭建Python3 基础语法Python3 基本数据类型Python3 数据类型转换Python3 解释器Python3 注释Python3 运算符Python3 数字(Number)Python3 字符串Python3 列表Python3 元组Python3 字典Python3 集…

dnSpy调试工具二次开发1-新增菜单

测试环境&#xff1a; window 10 visual studio 2019 版本号&#xff1a;16.11.15 .net framework 4.8 开发者工具包 下载 .NET Framework 4.8 | 免费官方下载 .net 5开发者工具包 下载 .NET 5.0 (Linux、macOS 和 Windows) 利用git拉取代码(源码地址&#xff1a;Gi…

启动IDEA报错,web servcer failed to start.port 8080 was already in use.

启动IDEA报错&#xff0c;web servcer failed to start.port 8080 was already in use. 问题现状 启动IDEA失败&#xff0c;端口被占用。 解决办法&#xff1a; 使用netstat -ano指令&#xff0c;查看端口占用情况 因为我是win11的系统&#xff0c;使用指令时出现如下提示。…

【IC设计】移位寄存器

目录 理论讲解背景介绍什么是移位寄存器按工作模式分类verilog语法注意事项 设计实例循环移位寄存器算术双向移位寄存器5位线性反馈移位寄存器伪随机码发生器3位线性反馈移位寄存器32位线性反馈移位寄存器串行移位寄存器&#xff08;打4拍&#xff09;双向移位寄存器&#xff1…

c语言题目之统计二级制数中1的个数

文章目录 题目一、方法1二、方法2三&#xff0c;方法3总结 题目 统计二进制数中1的个数 输入一行&#xff0c;输出一行 输入&#xff1a; 输入一个整数 输出&#xff1a; 输出存储在内存中二进制的1的个数 一、方法1 之前的文章中&#xff0c;小编写了有关于内存在二进制中的存…

Fiddler工具 — 8.会话列表(Session List)

1、会话列表说明 Fiddler抓取到的每条HTTP请求&#xff08;每一条称为一个session&#xff09;。 主要包含了请求的ID编号、状态码、协议、主机名、URL、内容类型、body大小、进程信息、自定义备注等信息。如下图&#xff1a; 说明&#xff1a; 名称含义#抓取HTTP Request的顺…

Ribbon相关问题及答案(2024)

1、Ribbon是什么&#xff0c;它在微服务架构中扮演什么角色&#xff1f; Ribbon是一个客户端负载均衡器&#xff0c;它在微服务架构中扮演着关键性的角色。Ribbon的设计理念是在客户端进行服务发现和负载均衡&#xff0c;这种方式不同于传统的通过中心化的负载均衡器&#xff…

YHZ018 Python 运算符优先级

资源编号&#xff1a;YHZ018 配套视频&#xff1a;https://www.bilibili.com/video/BV1zy4y1Z7nk?p19 YHZ018&#xff1a;运算符优先级 &#x1fabf; 运算符优先级 Python支持多种运算符&#xff0c;下表按照优先级从高到低的顺序列出了所有运算符。运算符的优先级决定了在表…

面试算法90:环形房屋偷盗

题目 一条环形街道上有若干房屋。输入一个数组表示该条街道上的房屋内财产的数量。如果这条街道上相邻的两幢房屋被盗就会自动触发报警系统。请计算小偷在这条街道上最多能偷取的财产的数量。例如&#xff0c;街道上5家的财产用数组[2&#xff0c;3&#xff0c;4&#xff0c;5…

js实现全选按钮,反选

点击全选按钮&#xff0c;下面的按钮全部选中&#xff1b;再次点击&#xff0c;全部取消选择。 点击下面的按钮时&#xff0c;检查下面的按钮是不是全部都选中&#xff0c;如果全部选中了&#xff0c;需要修改全选按钮的选中状态为ture。 全选反选 <!DOCTYPE html> <…

Linux系统IO—探索输入输出操作的奥秘

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;HEART BEAT—YOASOBI 2:20━━━━━━️&#x1f49f;──────── 5:35 &#x1f504; ◀️ ⏸ ▶️ ☰ …

c++之迭代器

目录 一、迭代器 二、几种常见的迭代器类型 三、使用迭代器时注意事项 一、迭代器 在C中&#xff0c;迭代器是一种用于遍历容器元素的对象。迭代器提供了一种通用的方式来访问各种不同类型的容器&#xff0c;如数组、向量、列表、集合和映射等。 使用迭代器可以避免直接操作…

三、Qt核心与Qt类库

一、Qt核心&#xff1a;元对象系统 1、Qt核心特点 Qt对标准C进行了扩展&#xff0c;引入了一些新的概念和功能元对象编译器&#xff08;MOC&#xff09;是一个预处理器&#xff0c;先将Qt的特性程序转为标准C程序&#xff0c;再由标准C编译器进行编译Qt为C语言增加的特性在Qt…

提升开发效率:npm包管理器的使用技巧

文章目录 一、npm简介二、npm的基本操作1. 安装Node.js和npm2. 创建和管理项目3. 安装依赖4. 卸载依赖5. 更新依赖 三、npm的高级特性1. 使用不同版本的依赖项2. 查看已安装的依赖项和它们的版本信息3. 运行脚本命令 《Node.js从入门到精通&#xff08;软件开发视频大讲堂&…

09-生成器模式(Builder)模式

意图 将一个复杂对象的构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 理解 如果构建一个对象的的过程会比较复杂&#xff0c;或者说在写代码的过程中&#xff0c;需要比较频繁地构建某个对象&#xff0c;那么可以针对这个对象写一个专门用于构建这…

2024前端炫酷源码分享(附效果图及在线演示)

分享10款非常有趣的前端特效源码 其中包含css动画特效、js原生特效、svg特效以及小游戏等 下面我会给出特效样式图或演示效果图 但你也可以点击在线预览查看源码的最终展示效果及下载源码资源 GSAP-火箭动画特效 GSAP 火箭动画 当氮气充足的情况下 火箭会冲出 并继续飞行 图片…

C#,字符串匹配算法(模式搜索)Z算法的源代码与数据可视化

Z算法也是模式搜索&#xff08;Pattern Search Algorithm&#xff09;的常用算法。 本文代码的运算效果&#xff1a; 一、Z 算法 线性时间模式搜索算法的Z算法&#xff0c;在线性时间内查找文本中模式的所有出现。 假设文本长度为 n&#xff0c;模式长度为 m&#xff0c;那么…

【Spring Cloud 】进阶之Config配置中心

目录 config大致的一个思路&#xff1a; 二&#xff0c;前期准备 2.1导入依赖 2.2编写bootstrop.yml&#xff1a; 三&#xff0c;编写Controller类 3.1获取单个配置类信息 3.2获取多个配置类信息 &#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f3…

Linux习题6

解析&#xff1a;排序必须得是rwx,所以B不对 解析&#xff1a; /etc/resolv.conf&#xff1a;是DNS配置文件。在网卡配置文件中进行配置&#xff0c;默认情况下&#xff0c;网卡配置文件DNS优于/etc/resolv.conf。 /etc/hostname&#xff1a;在centos7&#xff0c;配置主机名…