【方辉专栏】ARM嵌入式编译器(五) 优化循环的4种方法
发布时间:2022-08-16

摘要: 本文主要对Arm Compiler 6编译器的优化循环对编写优化代码的作用进行介绍。

关键字:Arm Compiler 6、编译器、优化循环 、循环展开、pragma、循环向量化、循环终止、无限循环、


1. 循环展开

循环执行的时间取决于循环的次数,循环中每次检查是否进行循环的条件会降低循环的性能。使用循环展开可以减少检查条件的判断次数,但是展开循环就意味着增加代码量。例如:在精确的时钟周期循环中,可以使用#pragma unroll (n)来展开循环。

“pragma”(编译指示)仅在选择优化等级为-O2/-O3/-Ofast和-Omax时有效。

编译指示的相关用法:

#pragma unroll (n)展开n次循环
#pragma unroll_completely展开所有循环

注:虽然给出了循环展开的编译指示,但Arm官方不建议使用,这样会影响编译器的展开优化和其他循环优化。

不使用循环展开的代码使用循环展开的代码
int countSetBits1(unsigned int n)
{
int bits = 0;
while (n != 0)
{
if (n & 1) bits++;
n >>= 1;
}
return bits;
}
int countSetBits2(unsigned int n)
{
int bits = 0;
#pragma unroll (4)
while (n != 0)
{
if (n & 1) bits++;
n >>= 1;
}
return bits;
}

将代码分别复制到file.c文件中,然后使用以下命令进行编译和反汇编。

armclang --target=arm-arm-none-eabi -march=armv8-a file.c -O2 -S -o file.s

不使用循环展开的汇编代码使用循环展开的汇编代码
countSetBits1:
mov r1, r0
mov r0, #0
cmp r1, #0
bxeq lr
mov r2, #0
mov r0, #0
.LBB0_1:
and r3, r1, #1
cmp r2, r1, asr #1
add r0, r0, r3
lsr r3, r1, #1
mov r1, r3
bne .LBB0_1
bx lr
countSetBits2:
mov r1, r0
mov r0, #0
cmp r1, #0
bxeq lr
mov r2, #0
mov r0, #0
LBB0_1:
and r3, r1, #1
cmp r2, r1, asr #1
add r0, r0, r3
beq .LBB0_4
@ BB#2:
asr r3, r1, #1
cmp r2, r1, asr #2
and r3, r3, #1
add r0, r0, r3
asrne r3, r1, #2
andne r3, r3, #1
addne r0, r0, r3
cmpne r2, r1, asr #3
beq .LBB0_4
@ BB#3:
asr r3, r1, #3
cmp r2, r1, asr #4
and r3, r3, #1
add r0, r0, r3
asr r3, r1, #4
mov r1, r3
bne .LBB0_1
.LBB0_4:
bx lr

可以看到展开循环时,代码执行会更快,但代码量也更大。


2. 循环向量化

如果编译的目标含有SIMD单元,那么编译器就可以使用向量引擎来优化代码的向量部分。在优化等级为-O1,可以使用-fvectorize 来启动优化,而在-O2或更高等级时向量优化是自动启用。

要使用向量优化,在编写代码的时候需要将结构体的成员放到同一个循环中,而不能使用独立的循环。

可以进行SIMD优化的代码不能进行SIMD优化的代码
typedef struct tBuffer {
int a;
int b;
int c;
} tBuffer;
tBuffer buffer[8];
void DoubleBuffer1 (void)
{
int i;
for (i=0; i<8; i++)
{
buffer[i].a *= 2;
buffer[i].b *= 2;
buffer[i].c *= 2;
}
}
typedef struct tBuffer {
int a;
int b;
int c;
} tBuffer;
tBuffer buffer[8];
void DoubleBuffer2 (void)
{
int i;
for (i=0; i<8; i++)
buffer[i].a *= 2;
for (i=0; i<8; i++)
buffer[i].b *= 2;
for (i=0; i<8; i++)
buffer[i].c *= 2;
}

对于每个例子,将代码分别复制到file.c文件中,然后使用以下命令进行编译和反汇编。

armclang --target=arm-arm-none-eabi -march=armv8-a file.c -O2 -S -o file.s

向量优化后汇编代码未进行向量优化的代码
DoubleBuffer1:
.fnstart
@ BB#0:
movw r0, :lower16:buffer
movt r0, :upper16:buffer
vld1.64 {d16, d17}, [r0:128]
mov r1, r0
vshl.i32 q8, q8, #1
vst1.32 {d16, d17}, [r1:128]!
vld1.64 {d16, d17}, [r1:128]
vshl.i32 q8, q8, #1
vst1.64 {d16, d17}, [r1:128]
add r1, r0, #32
vld1.64 {d16, d17}, [r1:128]
vshl.i32 q8, q8, #1
vst1.64 {d16, d17}, [r1:128]
add r1, r0, #48
vld1.64 {d16, d17}, [r1:128]
vshl.i32 q8, q8, #1
vst1.64 {d16, d17}, [r1:128]
add r1, r0, #64
add r0, r0, #80
vld1.64 {d16, d17}, [r1:128]
vshl.i32 q8, q8, #1
vst1.64 {d16, d17}, [r1:128]
vld1.64 {d16, d17}, [r0:128]
vshl.i32 q8, q8, #1
vst1.64 {d16, d17}, [r0:128]
bxlr
DoubleBuffer2:
.fnstart
@ BB#0:
movw r0, :lower16:buffer
movt r0, :upper16:buffer
ldr r1, [r0]
lsl r1, r1, #1
str r1, [r0]
ldr r1, [r0, #12]
lsl r1, r1, #1
str r1, [r0, #12]
ldr r1, [r0, #24]
lsl r1, r1, #1
str r1, [r0, #24]
ldr r1, [r0, #36]
lsl r1, r1, #1
str r1, [r0, #36]
ldr r1, [r0, #48]
lsl r1, r1, #1
str r1, [r0, #48]
ldr r1, [r0, #60]
lsl r1, r1, #1
str r1, [r0, #60]
ldr r1, [r0, #72]
lsl r1, r1, #1
str r1, [r0, #72]
ldr r1, [r0, #84]
lsl r1, r1, #1
str r1, [r0, #84]
ldr r1, [r0, #4]
lsl r1, r1, #1
str r1, [r0, #4]
ldr r1, [r0, #16]
lsl r1, r1, #1
...
bx lr

在64位运行状态下要避免编译器使用SIMD向量优化可以在-march或-mcpu后+nosimd;

例如:

armclang --target=aarch64-arm-none-eabi -march=armv8-a+nosimd -O2 file.c -S -o file.s

在32位运行状态下要避免编译器使用SIMD向量优化,可以通过设置-mfpu=fp-armv8;

例如:

armclang --target=aarch32-arm-none-eabi -march=armv8-a -mfpu=fp-armv8 -O2 file.c -S -o file.s


3. 循环终止

在写循环的时候如果编写不当会使得代码的运行效率降低和代码量增大。建议使用以下的终止条件:

1)使用变量类型为:unsigned int

2)使用向下减少的计数方式,以减到0作为计数结束。

3)使用简单的终止条件。

单独或组合使用以上原则的终止条件,可以获得更好的代码大小或效率。

例如:这是一个实现n!的计算程序。

递增循环递减循环
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=arm-arm-none-eabi -march=armv8-a

递增循环递减循环
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

对比反汇编代码可以看出在递减循环中用SUBS指令代替了递增循环中ADD 和CMP两条指令。这是因为SUBS指令会自动更新Z标志。

此外在递减循环中变量n不必再循环的过程实时使用,从而减少了寄存器的数量。

如果终止条件是一个函数,则循环的每次都调用该函数,这种情况下递减的循环优势就更明显了。例如:

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

说明:这种递减循环计数的方式也适用于while-do 命令。


4. 无限循环

在某些情况下armclang会删除一些编译器认为没有影响的无限循环,从而导致最终程序无法正常运行。

为确保无限循环的正确编译执行,ARM官方建议在无限循环中添加__arm volatile的声明。这个声明的目的是告诉编译器删除这个无限循环会有影响,不能被优化删除。在无限循环中,把处理器设置为低功耗模式是一个不错的做法,当有中断或事件触发时再回到正常模式。

下面是一个包含__arm volatile声明的无限循环例子:

void infinite_loop(void) {

while (1)

  __asm volatile("wfe");

}


注:wfe(Wait for Event)是给处理器一个提示,使处理器进入低功耗状态,直到事件或中断触发。


来源:《Arm® Compiler for Embedded User Guide Version 6.18》


+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


关于凯发k8一触即发电子

凯发k8一触即发电子技术有限公司(英文名称:Emdoor Electronics Technology Co.,Ltd)是国内资深的研发工具软件提供商,公司成立于 2002 年,面向中国广大的制造业客户提供研发、设计、管理过程中使用的各种软件开发工具,致力于帮助客户提高研发管理效率、缩短产品设计周期,提升产品可靠性。

20 年来,先后与 Altium、ARM、Ansys、QT、Adobe、Visu-IT、Minitab、Testplant、EPLAN、HighTec、GreenHills、PLS、Ashling、MSC Software 、Autodesk、Source Insight、TeamEDA、MicroFocus等多家全球知名公司建立战略合作伙伴关系,并作为他们在中国区的主要分销合作伙伴服务了数千家中国本土客户,为客户提供从芯片级开发工具、EDA 设计工具、软件编译以及测试工具、结构设计工具、仿真工具、电气设计工具、以及嵌入式 GUI 工具等等。凯发k8一触即发电子凭借多年的经验积累,真正的帮助客户实现了让研发更简单、更可靠、更高效的目标。

欢迎关注“凯发k8一触即发电子”公众号

了解更多研发工具软件知识