【问题标题】:Can modern compilers unroll `for` loops expressed using begin and end iterators现代编译器能否展开使用开始和结束迭代器表示的“for”循环
【发布时间】:2012-07-17 18:54:49
【问题描述】:

考虑下面的代码

 vector<double> v;
 // fill v
 const vector<double>::iterator end =v.end();
 for(vector<double>::iterator i = v.bgin(); i != end; ++i) {
   // do stuff
 }

像 g++、clang++、icc 这样的编译器是否能够展开这样的循环。不幸的是,我不知道程序集能够从输出中验证循环是否展开。 (而且我只能访问 g++。)

在我看来,这似乎需要编译器比平常更聪明,首先推断迭代器是随机访问迭代器,然后计算循环执行的次数。启用优化后编译器可以这样做吗?

感谢您的回复,在你们中的一些人开始讲授过早优化之前,这是一个好奇的练习。

【问题讨论】:

  • 我猜测由于无法预测大小,因此无需展开。
  • +1 用于预防过早优化的指控 :-)
  • 绝对可以在不知道行程次数的情况下展开。它只需要一些清理代码......我以前做过很多次。
  • @san 您将其定义为const,而不是常量。它的值是在运行时确定的,而不是在编译时确定的,因此不能使用 constness 来优化/展开循环。
  • MSVC 在我的任何测试中都没有展开这个循环(完全优化级别,有利于速度和 fp:fast)

标签: c++ stl loop-unrolling


【解决方案1】:

我建议编译器是否可以使用现代流水线架构和缓存展开循环,除非您的“做事”是微不足道的,否则这样做几乎没有什么好处,而且在许多情况下这样做会是一个性能命中而不是福音。如果您的“做事”不重要,展开循环将创建此重要代码的多个副本,这将需要额外的时间来加载到缓存中,从而显着减慢展开循环的第一次迭代。同时,它会从缓存中驱逐更多代码,如果它进行任何函数调用,这可能是执行“do stuff”所必需的,然后需要再次将其重新加载到缓存中。在无缓存流水线非分支预测架构之前,展开循环的目的很有意义,其目标是减少与循环逻辑相关的开销。现在使用基于缓存的流水线分支预测硬件,您的 cpu 将很好地流水线进入下一个循环迭代,推测性地再次执行循环代码,当您检测到 i==end 退出条件时,处理器将抛出出最终投机执行的结果集。在这样的架构中,循环展开几乎没有意义。它会进一步膨胀代码而几乎没有任何好处。

【讨论】:

  • +1,因为这很好地解释了为什么它通常不会展开循环,即使它可以。但是 OP 正在询问,如果它值得做的话,编译器是否能够这样做,我不确定如何在没有真正想象出一个值得做的案例的情况下完全回答这个问题……
  • @abarnert 我认为与volatile 合作确实有资格作为“微不足道的”,因此是一个很好的测试用例。想不出任何不会完全优化的更短的东西
  • @san:实际上,考虑到 user315052 的回答,从 volatile 复制 from 似乎是一个更好的测试用例。也许将内联汇编放在循环中会更简单?哦,好吧,我们的答案已经得到证实,所以似乎不需要更多的努力。
  • @abarnert 这也可能是编译器的决定因素,因为它们的指令数量应该是相当的。
  • @san:也许吧,但是 g++ 4.2 和 4.7 做一件事,而 4.1 和 4.4 做另一件事似乎有点奇怪。并非不可能,当然 apple-llvm-g++ 4.2 很容易与 stock 4.2 不同……
【解决方案2】:

在我看来,这似乎需要编译器比平时更聪明,首先推断迭代器是随机访问迭代器,然后计算循环执行的次数。

完全由模板组成的 STL 具有所有代码 inline。因此,当编译器开始应用优化时,随机访问迭代器已经减少为指针。创建 STL 的原因之一是为了减少程序员智取编译器的需要。您应该依靠 STL 来做正确的事情,直到证明并非如此。

当然,您仍然可以从 STL 中选择合适的工具来使用...

编辑:讨论过g++ 是否会展开任何循环。在我使用的版本中,循环展开不是-O-O2-O3 的一部分,我使用以下代码获得后两个级别的相同程序集:

void foo (std::vector<int> &v) {
    volatile int c = 0;
    const std::vector<int>::const_iterator end = v.end();
    for (std::vector<int>::iterator i = v.begin(); i != end; ++i) {
        *i = c++;
    }
}

用对应的程序集-O2程序集:

_Z3fooRSt6vectorIiSaIiEE:
.LFB435:
        movq    8(%rdi), %rcx
        movq    (%rdi), %rax
        movl    $0, -4(%rsp)
        cmpq    %rax, %rcx
        je      .L4
        .p2align 4,,10
        .p2align 3
.L3:
        movl    -4(%rsp), %edx
        movl    %edx, (%rax)
        addq    $4, %rax
        addl    $1, %edx
        cmpq    %rax, %rcx
        movl    %edx, -4(%rsp)
        jne     .L3
.L4:
        rep
        ret

添加-funroll-loops 选项后,该功能扩展为更大的东西。但是,文档警告了这个选项:

展开循环,其迭代次数可以在编译时或进入循环时确定。 -funroll-loops 意味着 -frerun-cse-after-loop。它还开启了完全循环剥离(即,以少量恒定迭代次数完全去除循环)。 此选项会使代码变大,可能会也可能不会使其运行得更快。

作为阻止您自己展开循环的进一步论据,我将通过将Duff's Device 应用于上面的foo 函数的说明来完成这个答案:

void foo_duff (std::vector<int> &v) {
    volatile int c = 0;
    const std::vector<int>::const_iterator end = v.end();
    std::vector<int>::iterator i = v.begin();
    switch ((end - i) % 4) do {
    case 0: *i++ = c++;
    case 3: *i++ = c++;
    case 2: *i++ = c++;
    case 1: *i++ = c++;
    } while (i != end);
}

GCC 有另一个循环优化标志:

-ftree-loop-optimize
在树上执行循环优化。此标志在-O 及更高版本中默认启用。

因此,-O 选项可以对最内层的循环进行简单的循环优化,包括对具有固定迭代次数的循环进行完整的循环展开(剥离)。 (感谢 doc 向我指出这一点。)

【讨论】:

  • 感谢您的详细回复。一个问题:const_iterator 应该是 const 迭代器吗?前者不是说被解引用的对象是常量,而迭代器不是吗?
  • @san:我使用const_iterator 表明我不打算使用end 来修改任何内容。您也可以使end const 表示变量本身不会改变。但我认为编译器可以从它是本地的,而不是在循环中使用的事实推断出这一点。
  • 对,我认为编译器从 end 不应该被取消引用的事实中找出相反的方法。但我想人们可以通过在容器之外取消引用来使用一些讨厌的技巧,所以编译器不能禁止它。
  • @san:不,我认为你是对的,编译器可以弄清楚,只要本地迭代器不通过引用传递给其他函数。但是,我添加了另一个const,只是为了你。
  • 啊哈。如果我将测试代码更改为从 volatile 复制到 *i,而不是相反,至少 apple-gcc-4.2 实际上确实展开了循环。很奇怪,这会有所作为,但是……这很好地说明了很难证明否定的事实(“编译器不能做 X,因为我无法想出一个让它做 X 的例子”没有' t 工作)而不是肯定的(“编译器可以做 X,因为我想出了一个让它做 X 的例子”)。 :)
【解决方案3】:

简短的回答是肯定的。它会尽可能多地展开。就您而言,这显然取决于您如何定义 end (我假设您的示例是通用的)。大多数现代编译器不仅会展开,而且还会进行矢量化并进行其他优化,这些优化通常会让您自己的解决方案彻底失败。

所以我的意思是不要过早优化!开个玩笑:)

【讨论】:

  • 我将 end 定义为循环外的常量迭代器。
  • 它不会尽可能多地展开;它会按照它认为值得展开的程度展开。在许多情况下,这根本不是。
  • 我不认为编译器能够像使用计数循环那样展开基于迭代器的循环。 v.end() 不是 constexpr,那么编译器如何知道编译时循环在哪里结束?也许通过一些疯狂的指针跟踪它可以优化本地对象,但是当v 是一个函数参数时这是完全不可能的。
【解决方案4】:

简单回答:通常否!至少在完整的循环展开时是这样。

让我们在这个简单的脏编码(用于测试目的)结构上展开测试循环。

struct Test
{
    Test(): begin(arr), end(arr + 4) {}

    double * begin;
    double * end;
    double arr[4];
};

首先让我们采用counted loop并在没有任何优化的情况下对其进行编译。

double counted(double param, Test & d)
{
    for (int i = 0; i < 4; i++)
        param += d.arr[i];
    return param;
}

这是 gcc 4.9 产生的结果。

counted(double, Test&):
    pushq   %rbp
    movq    %rsp, %rbp
    movsd   %xmm0, -24(%rbp)
    movq    %rdi, -32(%rbp)
    movl    $0, -4(%rbp)
    jmp .L2
.L3:
    movq    -32(%rbp), %rax
    movl    -4(%rbp), %edx
    movslq  %edx, %rdx
    addq    $2, %rdx
    movsd   (%rax,%rdx,8), %xmm0
    movsd   -24(%rbp), %xmm1
    addsd   %xmm0, %xmm1
    movq    %xmm1, %rax
    movq    %rax, -24(%rbp)
    addl    $1, -4(%rbp)
.L2:
    cmpl    $3, -4(%rbp)
    jle .L3
    movq    -24(%rbp), %rax
    movq    %rax, -40(%rbp)
    movsd   -40(%rbp), %xmm0
    popq    %rbp
    ret

正如预期的那样,循环还没有展开,而且由于没有进行任何优化,代码通常非常冗长。现在让我们打开-O3 标志。生产拆解:

counted(double, Test&):
    addsd   16(%rdi), %xmm0
    addsd   24(%rdi), %xmm0
    addsd   32(%rdi), %xmm0
    addsd   40(%rdi), %xmm0
    ret

瞧,这次循环已经展开。


现在让我们看一下迭代循环。包含循环的函数将如下所示。

double iterated(double param, Test & d)
{
  for (double * it = d.begin; it != d.end; ++it)
    param += *it;
  return param;
}

仍然使用-O3标志,我们来看看反汇编。

iterated(double, Test&):
    movq    (%rdi), %rax
    movq    8(%rdi), %rdx
    cmpq    %rdx, %rax
    je  .L3
.L4:
    addsd   (%rax), %xmm0
    addq    $8, %rax
    cmpq    %rdx, %rax
    jne .L4
.L3:
    rep ret

代码看起来比第一种情况更好,因为执行了优化,但这次没有展开循环!

funroll-loopsfunroll-all-loops 标志呢?他们会产生类似的结果

iterated(double, Test&):
    movq    (%rdi), %rsi
    movq    8(%rdi), %rcx
    cmpq    %rcx, %rsi
    je  .L3
    movq    %rcx, %rdx
    leaq    8(%rsi), %rax
    addsd   (%rsi), %xmm0
    subq    %rsi, %rdx
    subq    $8, %rdx
    shrq    $3, %rdx
    andl    $7, %edx
    cmpq    %rcx, %rax
    je  .L43
    testq   %rdx, %rdx
    je  .L4
    cmpq    $1, %rdx
    je  .L29
    cmpq    $2, %rdx
    je  .L30
    cmpq    $3, %rdx
    je  .L31
    cmpq    $4, %rdx
    je  .L32
    cmpq    $5, %rdx
    je  .L33
    cmpq    $6, %rdx
    je  .L34
    addsd   (%rax), %xmm0
    leaq    16(%rsi), %rax
.L34:
    addsd   (%rax), %xmm0
    addq    $8, %rax
.L33:
    addsd   (%rax), %xmm0
    addq    $8, %rax
.L32:
    addsd   (%rax), %xmm0
    addq    $8, %rax
.L31:
    addsd   (%rax), %xmm0
    addq    $8, %rax
.L30:
    addsd   (%rax), %xmm0
    addq    $8, %rax
.L29:
    addsd   (%rax), %xmm0
    addq    $8, %rax
    cmpq    %rcx, %rax
    je  .L44
.L4:
    addsd   (%rax), %xmm0
    addq    $64, %rax
    addsd   -56(%rax), %xmm0
    addsd   -48(%rax), %xmm0
    addsd   -40(%rax), %xmm0
    addsd   -32(%rax), %xmm0
    addsd   -24(%rax), %xmm0
    addsd   -16(%rax), %xmm0
    addsd   -8(%rax), %xmm0
    cmpq    %rcx, %rax
    jne .L4
.L3:
    rep ret
.L44:
    rep ret
.L43:
    rep ret

将结果与展开循环进行计数循环比较。显然不一样。我们在这里看到的是 gcc 将循环分成 8 个元素块。这在某些情况下可以提高性能,因为每 8 次正常循环迭代检查一次循环退出条件。还可以执行附加标志矢量化。但它不是完整的循环展开。

如果Test 对象不是函数参数,则会展开迭代循环。

double iteratedLocal(double param)
{
  Test d;
  for (double * it = d.begin; it != d.end; ++it)
    param += *it;
  return param;
}

仅使用-O3 标志生成的反汇编:

iteratedLocal(double):
    addsd   -40(%rsp), %xmm0
    addsd   -32(%rsp), %xmm0
    addsd   -24(%rsp), %xmm0
    addsd   -16(%rsp), %xmm0
    ret

如您所见,循环已展开。这是因为编译器现在可以安全地假定 end 具有固定值,而它无法预测函数参数。

Test 结构是静态分配的。对于像std::vector 这样的动态分配结构,事情会变得更加复杂。从我对修改后的Test 结构的观察来看,它类似于动态分配的容器,看起来 gcc 尽力展开循环,但在大多数情况下生成的代码并不像上面那样简单。


当您询问其他编译器时,这里是 clang 3.4.1 的输出(-O3 标志)

counted(double, Test&):                      # @counted(double, Test&)
    addsd   16(%rdi), %xmm0
    addsd   24(%rdi), %xmm0
    addsd   32(%rdi), %xmm0
    addsd   40(%rdi), %xmm0
    ret

iterated(double, Test&):                     # @iterated(double, Test&)
    movq    (%rdi), %rax
    movq    8(%rdi), %rcx
    cmpq    %rcx, %rax
    je  .LBB1_2
.LBB1_1:                                # %.lr.ph
    addsd   (%rax), %xmm0
    addq    $8, %rax
    cmpq    %rax, %rcx
    jne .LBB1_1
.LBB1_2:                                # %._crit_edge
    ret

iteratedLocal(double):                     # @iteratedLocal(double)
    leaq    -32(%rsp), %rax
    movq    %rax, -48(%rsp)
    leaq    (%rsp), %rax
    movq    %rax, -40(%rsp)
    xorl    %eax, %eax
    jmp .LBB2_1
.LBB2_2:                                # %._crit_edge4
    movsd   -24(%rsp,%rax), %xmm1
    addq    $8, %rax
.LBB2_1:                                # =>This Inner Loop Header: Depth=1
    movaps  %xmm0, %xmm2
    cmpq    $24, %rax
    movaps  %xmm1, %xmm0
    addsd   %xmm2, %xmm0
    jne .LBB2_2
    ret

英特尔的 icc 13.01(-O3 标志)

counted(double, Test&):
        addsd     16(%rdi), %xmm0                               #24.5
        addsd     24(%rdi), %xmm0                               #24.5
        addsd     32(%rdi), %xmm0                               #24.5
        addsd     40(%rdi), %xmm0                               #24.5
        ret                                                     #25.10
iterated(double, Test&):
        movq      (%rdi), %rdx                                  #30.26
        movq      8(%rdi), %rcx                                 #30.41
        cmpq      %rcx, %rdx                                    #30.41
        je        ..B3.25       # Prob 50%                      #30.41
        subq      %rdx, %rcx                                    #30.7
        movb      $0, %r8b                                      #30.7
        lea       7(%rcx), %rax                                 #30.7
        sarq      $2, %rax                                      #30.7
        shrq      $61, %rax                                     #30.7
        lea       7(%rax,%rcx), %rcx                            #30.7
        sarq      $3, %rcx                                      #30.7
        cmpq      $16, %rcx                                     #30.7
        jl        ..B3.26       # Prob 10%                      #30.7
        movq      %rdx, %rdi                                    #30.7
        andq      $15, %rdi                                     #30.7
        je        ..B3.6        # Prob 50%                      #30.7
        testq     $7, %rdi                                      #30.7
        jne       ..B3.26       # Prob 10%                      #30.7
        movl      $1, %edi                                      #30.7
..B3.6:                         # Preds ..B3.5 ..B3.3
        lea       16(%rdi), %rax                                #30.7
        cmpq      %rax, %rcx                                    #30.7
        jl        ..B3.26       # Prob 10%                      #30.7
        movq      %rcx, %rax                                    #30.7
        xorl      %esi, %esi                                    #30.7
        subq      %rdi, %rax                                    #30.7
        andq      $15, %rax                                     #30.7
        negq      %rax                                          #30.7
        addq      %rcx, %rax                                    #30.7
        testq     %rdi, %rdi                                    #30.7
        jbe       ..B3.11       # Prob 2%                       #30.7
..B3.9:                         # Preds ..B3.7 ..B3.9
        addsd     (%rdx,%rsi,8), %xmm0                          #31.9
        incq      %rsi                                          #30.7
        cmpq      %rdi, %rsi                                    #30.7
        jb        ..B3.9        # Prob 82%                      #30.7
..B3.11:                        # Preds ..B3.9 ..B3.7
        pxor      %xmm6, %xmm6                                  #28.12
        movaps    %xmm6, %xmm7                                  #28.12
        movaps    %xmm6, %xmm5                                  #28.12
        movsd     %xmm0, %xmm7                                  #28.12
        movaps    %xmm6, %xmm4                                  #28.12
        movaps    %xmm6, %xmm3                                  #28.12
        movaps    %xmm6, %xmm2                                  #28.12
        movaps    %xmm6, %xmm1                                  #28.12
        movaps    %xmm6, %xmm0                                  #28.12
..B3.12:                        # Preds ..B3.12 ..B3.11
        addpd     (%rdx,%rdi,8), %xmm7                          #31.9
        addpd     16(%rdx,%rdi,8), %xmm6                        #31.9
        addpd     32(%rdx,%rdi,8), %xmm5                        #31.9
        addpd     48(%rdx,%rdi,8), %xmm4                        #31.9
        addpd     64(%rdx,%rdi,8), %xmm3                        #31.9
        addpd     80(%rdx,%rdi,8), %xmm2                        #31.9
        addpd     96(%rdx,%rdi,8), %xmm1                        #31.9
        addpd     112(%rdx,%rdi,8), %xmm0                       #31.9
        addq      $16, %rdi                                     #30.7
        cmpq      %rax, %rdi                                    #30.7
        jb        ..B3.12       # Prob 82%                      #30.7
        addpd     %xmm6, %xmm7                                  #28.12
        addpd     %xmm4, %xmm5                                  #28.12
        addpd     %xmm2, %xmm3                                  #28.12
        addpd     %xmm0, %xmm1                                  #28.12
        addpd     %xmm5, %xmm7                                  #28.12
        addpd     %xmm1, %xmm3                                  #28.12
        addpd     %xmm3, %xmm7                                  #28.12
        movaps    %xmm7, %xmm0                                  #28.12
        unpckhpd  %xmm7, %xmm0                                  #28.12
        addsd     %xmm0, %xmm7                                  #28.12
        movaps    %xmm7, %xmm0                                  #28.12
..B3.14:                        # Preds ..B3.13 ..B3.26
        lea       1(%rax), %rsi                                 #30.7
        cmpq      %rsi, %rcx                                    #30.7
        jb        ..B3.25       # Prob 50%                      #30.7
        subq      %rax, %rcx                                    #30.7
        cmpb      $1, %r8b                                      #30.7
        jne       ..B3.17       # Prob 50%                      #30.7
..B3.16:                        # Preds ..B3.17 ..B3.15
        xorl      %r8d, %r8d                                    #30.7
        jmp       ..B3.21       # Prob 100%                     #30.7
..B3.17:                        # Preds ..B3.15
        cmpq      $2, %rcx                                      #30.7
        jl        ..B3.16       # Prob 10%                      #30.7
        movq      %rcx, %r8                                     #30.7
        xorl      %edi, %edi                                    #30.7
        pxor      %xmm1, %xmm1                                  #28.12
        lea       (%rdx,%rax,8), %rsi                           #31.19
        andq      $-2, %r8                                      #30.7
        movsd     %xmm0, %xmm1                                  #28.12
..B3.19:                        # Preds ..B3.19 ..B3.18
        addpd     (%rsi,%rdi,8), %xmm1                          #31.9
        addq      $2, %rdi                                      #30.7
        cmpq      %r8, %rdi                                     #30.7
        jb        ..B3.19       # Prob 82%                      #30.7
        movaps    %xmm1, %xmm0                                  #28.12
        unpckhpd  %xmm1, %xmm0                                  #28.12
        addsd     %xmm0, %xmm1                                  #28.12
        movaps    %xmm1, %xmm0                                  #28.12
..B3.21:                        # Preds ..B3.20 ..B3.16
        cmpq      %rcx, %r8                                     #30.7
        jae       ..B3.25       # Prob 2%                       #30.7
        lea       (%rdx,%rax,8), %rax                           #31.19
..B3.23:                        # Preds ..B3.23 ..B3.22
        addsd     (%rax,%r8,8), %xmm0                           #31.9
        incq      %r8                                           #30.7
        cmpq      %rcx, %r8                                     #30.7
        jb        ..B3.23       # Prob 82%                      #30.7
..B3.25:                        # Preds ..B3.23 ..B3.21 ..B3.14 ..B3.1
        ret                                                     #32.14
..B3.26:                        # Preds ..B3.2 ..B3.6 ..B3.4    # Infreq
        movb      $1, %r8b                                      #30.7
        xorl      %eax, %eax                                    #30.7
        jmp       ..B3.14       # Prob 100%                     #30.7
iteratedLocal(double):
        lea       -8(%rsp), %rax                                #8.13
        lea       -40(%rsp), %rdx                               #7.11
        cmpq      %rax, %rdx                                    #33.41
        je        ..B4.15       # Prob 50%                      #33.41
        movq      %rax, -48(%rsp)                               #32.12
        movq      %rdx, -56(%rsp)                               #32.12
        xorl      %eax, %eax                                    #33.7
..B4.13:                        # Preds ..B4.11 ..B4.13
        addsd     -40(%rsp,%rax,8), %xmm0                       #34.9
        incq      %rax                                          #33.7
        cmpq      $4, %rax                                      #33.7
        jb        ..B4.13       # Prob 82%                      #33.7
..B4.15:                        # Preds ..B4.13 ..B4.1
        ret                                                     #35.14

为了避免误解。如果计数循环条件将依赖于像这样的外部参数。

double countedDep(double param, Test & d)
{
    for (int i = 0; i < d.size; i++)
        param += d.arr[i];
    return param;
}

这样的循环也不会展开。

【讨论】:

  • "...它不是这样的循环展开..." 只是一个错误的陈述,除非您对 循环展开 有一个非常奇怪的定义。
  • @jxh 如果它仍然是一个循环,那么它是如何为您展开的?它只是被分成 8 个元素块,并以额外的代码和条件为代价。
  • Wikipedia 解释了 loop unrolling 的含义,它并不像您认为的那样。
  • @jxh 是的,但它不是完整的循环展开。这就是我所说的“循环展开”的意思。
  • 我认为很明显,除非编译器确切知道将执行多少次迭代,否则编译器无法“将循环完全展开为没有任何循环的东西”。对我来说也很明显,除非迭代次数非常少,否则完全展开循环是毫无意义的优化,所以我不确定你的狭隘观点是否比我的正确答案有任何改进。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-04-13
  • 2019-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多