现代编译器能够展开使用begin和end迭代器表示的for循环吗?

13

考虑以下代码

 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 ++。)

对我来说,这似乎需要编译器比平常更聪明一些,首先推断迭代器是随机访问迭代器,然后确定循环执行的次数。当启用优化时,编译器是否可以做到这一点?

感谢您的回复,在某些人开始讲述关于过早优化的演讲之前,这是一场好奇心的练习。


2
我猜测无法展开循环,因为大小无法预测。 - user195488
4
对于预防过早优化指责的主张,我会给予+1的支持 :-) - Aasmund Eldhuset
3
不知道循环次数也可以展开循环,只需要添加一些清理代码即可。我以前经常这样做过。 - Mysticial
2
@san 你将它定义为const,而不是常量。它的值在运行时确定,而不是编译时,因此常量性无法用于优化/展开循环。 - Jonathan Grynspan
2
在我的所有测试中,MSVC都没有展开这个循环(全优化级别,偏向速度和fp:fast)。 - harold
显示剩余14条评论
4个回答

8
对我而言,这需要编译器有比平时更聪明的能力,首先要推断出迭代器是随机访问迭代器,然后计算循环执行的次数。
STL完全由模板组成,所以所有代码都是内联的。因此,当编译器开始应用优化时,随机访问迭代器已经简化为指针。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向我指出这一点。)


@san:我使用了const_iterator来表明我不打算使用end来修改任何内容。你也可以将end设为const,以表示变量本身不会改变。但我认为编译器可以从它是局部变量且未在循环中使用这一事实中推断出来。 - jxh
我原本以为编译器会从end不应该被解引用这个事实反推出正确的结果。但我猜想,有些人可能会通过在容器范围之外进行解引用等恶意操作来欺骗编译器,使其无法禁止这种行为。 - san
@san:不,我认为你是对的,只要本地迭代器没有通过引用传递给其他函数,编译器就可以解决它。但是,我为了你加入了另一个“const”。 - jxh
1
啊哈。如果我将我的测试代码更改为从volatile复制到*i,而不是相反的方式,至少apple-gcc-4.2实际上会展开循环。奇怪的是这会有所不同,但这是一个很好的例子,说明证明否定命题比证明肯定命题更难(“编译器不能做X,因为我找不到一个使它做X的例子”行不通)。 :) - abarnert
@doc: 谢谢,这很有用! - jxh
显示剩余25条评论

8
我认为,无论编译器是否能展开循环,在现代流水线体系结构和缓存的情况下,除非你的“处理”是微不足道的,否则展开循环的好处很少,而在许多情况下,这样做会导致性能下降而不是提高。如果你的“处理”是复杂的,展开循环将创建多个此复杂代码的副本,这需要额外的时间加载到缓存中,显著减慢展开循环的第一次迭代。同时,它将从缓存中驱逐更多的代码,这可能对执行“处理”(如果有任何函数调用)所需的代码非常必要,这些代码然后需要重新加载到缓存中。展开循环的目的在没有缓存的流水线非分支预测体系结构中是有意义的,其目标是减少与循环逻辑相关的开销。现在,使用基于缓存的流水线分支预测硬件,当你检测到 i==end 退出条件时,你的 CPU 将被流水线化到下一个循环迭代,再次推测执行循环代码,此时处理器将放弃那最后一组推测执行的结果。在这样的体系结构中,展开循环几乎没有意义。它会进一步膨胀代码,却几乎没有好处。

2
+1,因为这是一个很好的解释,为什么即使可以,通常也不会展开循环。但是原帖的问题是,如果有必要展开循环,编译器是否能够这样做,我不确定如何在没有真正构思值得这样做的情况下完全回答这个问题... - abarnert
@abarnert 我认为将内容复制到“volatile”确实可以被视为“微不足道”,因此是一个很好的测试案例。我想不出任何更短的东西,它不会被完全优化掉。 - san
@san:实际上,鉴于user315052的回答,从volatile中复制似乎是更好的测试用例。也许在循环内部放置inline asm会更加微不足道?哦,好吧,我们已经证明了答案,所以似乎不需要再付出更多努力了。 - abarnert
@abarnert,也有可能决定因素是编译器,因为从指令数量上来看,它们应该是相当等效的。 - san
@san: 也许吧,但是g++ 4.2和4.7做一件事情,而4.1和4.4做另一件事情似乎有点奇怪。虽然不是不可能的,当然apple-llvm-g++ 4.2可能与原始的4.2有所不同... - abarnert
显示剩余2条评论

1

简单回答: 一般不行!至少在完全展开循环时是这样。

让我们在这个简单、脏代码(为了测试目的)结构上测试循环展开。

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

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

首先,让我们采用计数循环,并在没有任何优化的情况下对其进行编译。

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

英译中:Intel的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
1
我认为很明显编译器无法“将一个循环完全展开成没有任何循环的东西”,除非它确切地知道将执行多少次迭代。对我来说,完全展开循环是一种毫无意义的优化,除非迭代次数非常小,因此我不确定你的狭隘观点是否比我的正确答案有所改进。 - jxh
@jxh,你为什么写了“循环展开不是O、O2、O3的一部分”,而它确实是呢?编译器选择不展开迭代循环,因为这是没有意义的,而选择展开计数循环,所以你的回答最多只能算是含糊其辞。通常情况下,编译器不会展开由迭代器表示的循环。 - mip
我陈述了一个关于GCC如何工作的正确事实。文档列出了每个优化级别启用的选项,而-funroll-loops不属于其中任何一个。你展示的不是大多数人认为的循环展开(loop unrolling),虽然“完全循环展开”是你在这篇文章中发明的术语。 - jxh
@jxh,如果我的示例在使用O3标志编译时不是循环展开,那么你认为它是什么? - mip
显示剩余10条评论

1
简短的回答是肯定的。它会尽可能展开。在您的情况下,显然取决于您如何定义“end”(我假设您的示例是通用的)。大多数现代编译器不仅会展开,而且还会进行矢量化和其他优化,这往往会使您自己的解决方案变得毫无意义。
所以我的意思是,不要过早地进行优化!只是开个玩笑 :)

我将end定义为循环外的常量迭代器。 - san
它不会展开尽可能多的内容;它只会展开它认为值得展开的内容。在许多情况下,这将是根本不展开。 - abarnert
我认为编译器无法像计数循环一样展开基于迭代器的循环。v.end()不是constexpr,所以编译器怎么能在编译时知道循环何时结束呢?也许通过指针的疯狂跟踪,它可以优化本地对象,但当v是函数参数时,这是完全不可能的。 - mip

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接