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

您所在的位置:网站首页 keil5编译太慢了 Keil5,ARM编译器 软件优化注意事项

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

2024-07-10 13:10| 来源: 网络整理| 查看: 265

优化C代码中的环路终止

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

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

使用简单的终止条件。

写入倒计时到零循环。

使用 unsigned int 类型的计数器。

测试与零的相等性。

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

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

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

递增循环递减循环 int fact1(int n) { int i, fact = 1; for (i = 1; i >= 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 链接以列出所有全局符号的堆栈使用情况。

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

使用调试器,然后:

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

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

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

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

计算垃圾值的数量,然后乘以 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 代码行,可以识别源代码中的违规行。

 



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3