2

考虑这个 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 back 循环被分支中断?

4

4 回答 4

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
于 2012-04-24T16:06:03.703 回答
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;
}

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

于 2012-04-24T16:06:32.550 回答
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 值是常量。因此,它可以执行窥孔优化并结合恒定折叠以产生单次加法。如果窥视孔优化也考虑声明,那么它甚至可以进一步优化为单个移动指令。

于 2012-04-24T16:10:54.193 回答
0

对此没有一般的答案,不同的编译器,不同的版本,不同的编译器标志会有所不同。Use the appropriate option of your compiler to look at the assembler outcome. 使用 gcc 和亲戚,这是-S选项。

于 2012-04-24T15:55:57.367 回答