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

时间:2012-04-24 15:45:02

标签: c assembly

考虑这个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返回周期被分支中断?

4 个答案:

答案 0 :(得分:10)

这更多是为了演示编译器能够,而不是每个编译器会做什么。来源:

#include <stdio.h>

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

答案 1 :(得分:2)

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

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

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

答案 2 :(得分:2)

  

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

这种优化通常在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?

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

答案 3 :(得分:0)

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