循环展开优化,这是如何工作的

考虑这个C代码:

int sum=0; for(int i=0;i<5;i++) sum+=i; 

这可以通过这种方式转换为(伪)汇编(无需循环展开):

 % pseudo-code assembly ADDI $R10, #0 % sum ADDI $R11, #0 % i LOOP: ADD $R10, $R11 ADDI $R11, #1 BNE $R11, #5 LOOP 

所以我的第一个问题是如何在这两种方式之间使用循环展开来翻译此代码:

1)

 ADDI $R10, #0 ADDI $R10, #0 ADDI $R10, #1 ADDI $R10, #2 ADDI $R10, #3 ADDI $R10, #4 

2)

  ADD $R10, #10 

编译器是否能够优化代码并直接知道它必须在不执行所有总和的情况下添加10?

此外,是否有可能使用分支指令阻止管道? 我必须这样写吗:

 % pseudo-code assembly ADDI $R10, #0 % sum ADDI $R11, #0 % i LOOP: ADD $R10, $R11 ADDI $R11, #1 NOP % is this necessary to avoid the pipeline blocking? NOP NOP NOP BNE $R11, #5 LOOP 

为了避免fetch-decode-exe-mem-write返回周期被分支中断?

这更多是为了演示编译器的function ,而不是每个编译器的function。 来源:

 #include  int main(void) { int i, sum = 0; for(i=0; i<5; i++) { sum+=i; } printf("%d\n", sum); return 0; } 

请注意我添加的printf 。 如果未使用该变量,编译器将优化整个循环。

使用-O0进行编译(无优化)

gcc -Wall -O0 -S -c lala.c

 .L3: movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) .L2: cmpl $4, -8(%rbp) jle .L3 

循环以'哑'方式发生, -8(%rbp)是变量i

使用-O1进行编译(优化级别1)

gcc -Wall -O1 -S -c lala.c

 movl $10, %edx 

循环已完全删除并替换为等效值。


在展开时,编译器会查看将发生多少次迭代,并尝试通过执行更少的迭代来展开。 例如,循环体可能重复两次,这将导致分支数量减半。 C中的这种情况:

 int i = 0, sum = 0; sum += i; i++; for(; i<5;i++) { sum+=i; i++; sum+=i; } 

请注意,必须从循环中提取一次迭代。 这是因为5是奇数,因此通过复制内容不能简单地减半。 在这种情况下,循环只会输入两次。 -O0生成的汇编代码:

  movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) jmp .L2 .L3: movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) .L2: cmpl $4, -8(%rbp) 

完全展开C:

 for(i=0; i<5;i++) { sum+=i; i++; sum+=i; i++; sum+=i; i++; sum+=i; i++; sum+=i; } 

这次循环实际上只输入一次。 使用-O0生成的程序集:

 .L3: movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) movl -8(%rbp), %eax addl %eax, -4(%rbp) addl $1, -8(%rbp) .L2: cmpl $4, -8(%rbp) jle .L3 

所以我的第一个问题是在这两种方式之间如何使用循环展开来翻译此代码

这种优化通常在AST级别而不是输出代码(例如汇编)级别上实现。 当迭代次数是固定的并且在编译时已知时,可以完成循环展开。 所以例如我有这个AST:

 Program | +--For | +--Var | | | +--Variable i | +--Start | | | +--Constant 1 | +--End | | | +--Constant 3 | +--Statements | + Print i 

编译器会知道For的Start和End是常量,因此可以轻松复制语句,将Var的所有出现替换为每次调用的值。 对于上述AST,它将被翻译为:

 Program | +--Print 1 | +--Print 2 | +--Print 3 

编译器是否能够优化代码并直接知道它必须在不执行所有总和的情况下添加10?

是的,如果它被实现具有这样的function。 这实际上是对上述情况的改进。 在您的示例中,在执行展开之后,编译器可以看到所有l值保持不变,而r值是常量。 因此,它可以执行窥孔优化与恒定折叠相结合以产生单次添加。 如果窥视孔优化也考虑了声明,那么甚至可以将其优化为单个移动指令。

在基本级别,循环展开的概念只是简单地多次复制循环体。 编译器也可以进行其他优化(例如从计算中插入固定值),但不会被视为展开循环但可能将它们全部替换在一起。 但这最终将取决于所使用的编译器和标志。

C代码(仅展开)看起来更像这样:

 int sum = 0; int i = 0; for ( ; i < (5 & ~(4-1)); i += 4) /* unrolling 4 iterations */ { sum+=(i+0); sum+=(i+1); sum+=(i+2); sum+=(i+3); } for ( ; i < 5; i++) { sum+=i; } 

虽然编译器有很多机会在这里进行更多优化,但这只是一步。

对此没有一般的答案,不同的编译器,它们的不同版本,不同的编译器标志会有所不同。 使用编译器的相应选项查看汇编程序结果。 使用gcc和亲属,这是-S选项。