跳到主要内容

4.2 循环优化

循环的执行时间可能会随着迭代次数的增加而显著增长。如果每次迭代都要检查循环条件,这种开销可能会降低循环的执行性能。

4.2.1 循环展开​

你可以通过展开部分循环迭代来减少循环条件检查带来的开销,从而降低条件判断的次数。在源代码中,可以使用如下指令来展开时间关键的循环:

#pragma unroll(n)

然而,展开循环的缺点是会增加代码体积。这些 #pragma 指令仅在-O2、-Os优化等级下有效。

表4.2 循环展开的pragma指令

Pragma描述
#pragma unroll (n)展开循环的 n 次迭代

4.2.2 C代码中的循环终止​

如果编写不当,循环的终止条件可能会导致显著的开销。尽可能地应遵循以下做法:

  • 使用简单的终止条件
  • 编写递减至零(count-down-to-zero)循环,并测试是否等于零
  • 使用 unsigned int 类型的计数器

遵循其中一条或多条指南(单独或组合使用)通常能够生成更高效的代码。

以下实例展示了循环不展开和循环展开所生成代码的不同:

无循环展开的代码:

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;
}

使用下面的命令编译上面的代码:

kf32clang --target=kf32r -march=kf32r -O2 -S file.c

生成的汇编语言文件如下所示。

未循环展开的代码:

countSetBits1:
CMP R0, #0
JZ .LBB0_4
MOVL R4, #0
MOVL R2, #1
MOV R3, R0
.LBB0_2:
ANL R5, R0, R2
ADD R1, R4, R5
LSR R3, #1
CMP R0, #1
MOV R4, R1
MOV R0, R3
JHI .LBB0_2
MOV R0, R1
JMP LR
.LBB0_4:
MOVL R0, #0
JMP LR

循环展开后的代码:

countSetBits2:
MOVL R1, #0
CMP R0, #0
JZ .LBB0_6
MOVL R2, #1
.LBB0_2:
ANL R4, R0, R2
ADD R3, R1, R4
CMP R0, #2
JNC .LBB0_7
MOV R1, R0
LSR R1, #1
ANL R4, R1, R2
ADD R1, R3, R4
CMP R0, #4
JNC .LBB0_6
MOV R3, R0
LSR R3, #2
ANL R4, R3, R2
ADD R3, R1, R4
CMP R0, #8
JNC .LBB0_7
MOV R1, R0
LSR R1, #3
ANL R4, R1, R2
ADD R1, R3, R4
MOV R3, R0
LSR R3, #4
CMP R0, #15
MOV R0, R3
JHI .LBB0_2
.LBB0_6:
MOV R0, R1
JMP LR
.LBB0_7:
MOV R0, R3
JMP LR

通过比较可以发现,使用循环展开的函数countSetBits2,生成的代码运行速度更快但是体积也更大。