国家网站后缀,域名查询备案,广东网站关键词排名,网站建设设计制作 熊掌号优化C代码中的环路终止
循环是大多数程序中的常见结构。由于大量的执行时间通常花费在循环中#xff0c;因此值得关注时间关键循环。
如果不谨慎地编写#xff0c;环路终止条件可能会导致大量开销。在可能的情况下#xff1a; 使用简单的终止条件。 写入倒计时到零循环。…优化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 --targetarmv8a-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 --targetarmv8a-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 lrcountbit2: 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 --targetarmv8a-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 lrread_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 中还包括类。 有些堆栈使用并不明显例如 如果局部整数或浮点变量溢出即未分配给寄存器则会为其分配堆栈内存。 结构通常分配给堆栈。堆栈上保留了一个等效于 sizeofstruct 的空间该空间填充为 16 个字节的倍数。编译器尝试将结构分配给寄存器。 如果在编译时已知数组大小的大小则编译器会在堆栈上分配内存。同样在堆栈上保留了一个等效于 sizeofstruct 的空间该空间填充为 16 个字节的倍数。 注意 可变长度数组的内存在运行时在堆上分配。 一些优化可以引入新的临时变量来保存中间结果。优化包括CSE 消除、实时范围拆分和结构拆分。编译器尝试将这些临时变量分配给寄存器。如果没有它会将它们溢出到堆栈中。 通常为仅支持 16 位编码的 Thumb 指令的处理器编译的代码比 A64 代码、ARM 代码和为支持 32 位编码的 Thumb 指令的处理器编译的代码更多地使用堆栈。这是因为 16 位编码的 Thumb 指令只有 8 个寄存器可供分配而 ARM 代码和 32 位编码的 Thumb 指令则有 14 个寄存器。 AAPCS64要求通过堆栈而不是寄存器传递某些函数参数具体取决于它们的类型、大小和顺序。 估算堆栈使用情况的方法 堆栈使用情况很难估计因为它依赖于代码并且根据程序在执行时采用的代码路径在运行之间可能会有所不同。但是可以使用以下方法手动估计堆栈利用率的程度 使用 --callgraph 链接以生成静态调用图。这显示了有关所有功能的信息包括堆栈使用情况。 这将使用 .debug_frame 部分中的 DWARF 帧信息。使用 -g 选项进行编译以生成必要的 DWARF 信息。 使用 --infostack 或 --infosummarystack 链接以列出所有全局符号的堆栈使用情况。 使用调试器在堆栈中的最后一个可用位置设置观察点并查看是否命中了观察点。 使用调试器然后 在内存中为比预期需要的堆栈大得多的堆栈分配空间。 用已知值的副本填充堆栈空间例如 0xDEADDEAD。 运行应用程序或应用程序的固定部分。目标是在测试运行中使用尽可能多的堆栈空间。例如尝试执行最深嵌套的函数调用和静态分析找到的最坏情况路径。尝试在适当的位置生成中断以便将它们包含在堆栈跟踪中。 应用程序完成执行后检查内存的堆栈空间查看有多少已知值已被覆盖。该空间在已使用部分中有垃圾其余部分有已知值。 计算垃圾值的数量然后乘以 sizeofvalue以给出它们的大小以字节为单位。 计算结果显示了堆栈大小是如何增长的以字节为单位。 使用固定虚拟平台 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_raiseSIGFPE 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_raiseSIGFPE DIVBYZERO。因此如果重新实现 __rt_raise则必须选中 signal SIGFPE type DIVBYZERO 以确定是否发生了除以零的情况。
识别 C 代码中的整数除以零错误
进入 __aeabi_idiv0 时链路寄存器 LR 包含应用程序代码中调用 __aeabi_uidiv 除法例程后的指令地址。
通过在调试器中查找 LR 给出的地址处的 C 代码行可以识别源代码中的违规行。