【问题标题】:Loop unrolling optimization, how does this work循环展开优化,这是如何工作的
【发布时间】:2012-04-24 15:45:01
【问题描述】:

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

【问题讨论】:

    标签: c assembly


    【解决方案1】:

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

    #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
    

    【讨论】:

      【解决方案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 而不执行所有求和?

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

      【讨论】:

        【解决方案3】:

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

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

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

        【讨论】:

        • 在您展开的版本中,sum 最后将为 0。我认为你在某个地方出错了。循环只输入一次(这很好),但i 永远不会增加。
        • 是的,我忘了为展开的迭代增加索引变量。感谢您指出这一点。
        【解决方案4】:

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

        【讨论】:

        • 这应该是一条评论,因为它没有回答问题。
        • 它说在哪里可以找到答案。
        • 这就是 cmets 应该做的事情,有相同问题的人会寻找明确、明确的问题答案。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-12-03
        • 2014-09-07
        • 2011-05-30
        相关资源
        最近更新 更多