循环展开 - G++ 与 Clang++

Loop unrolling - G++ vs. Clang++

我想知道是否值得用模板帮助编译器展开一个简单的循环。我准备了以下测试:

#include <cstdlib>
#include <utility>
#include <array>

class TNode
{
public:
  void Assemble();
  void Assemble(TNode const *);
};

class T
{
private:
  std::array<TNode *,3u> NodePtr;

private:
  template <std::size_t,std::size_t>
  void foo() const;

  template <std::size_t... ij>
  void foo(std::index_sequence<ij...>) const
    { (foo<ij%3u,ij/3u>(),...); }

public:
  void foo() const
    { return foo(std::make_index_sequence<3u*3u>{}); }

  void bar() const;
};

template <std::size_t i,std::size_t j>
inline void T::foo() const
{
if constexpr (i==j)
  NodePtr[i]->Assemble();
else
  NodePtr[i]->Assemble(NodePtr[j]);
}

inline void T::bar() const
{
for (std::size_t i= 0u; i<3u; ++i)
  for (std::size_t j= 0u; j<3u; ++j)
    if (i==j)
      NodePtr[i]->Assemble();
    else
      NodePtr[i]->Assemble(NodePtr[j]);
}

void foo()
{
T x;
x.foo();
}

void bar()
{
T x;
x.bar();
}

我首先尝试使用启用了 -O3 -funroll-loops 的 G++,我得到了 (https://godbolt.org/z/_Wyvl8):

foo():
        push    r12
        push    rbp
        push    rbx
        sub     rsp, 32
        mov     r12, QWORD PTR [rsp]
        mov     rdi, r12
        call    TNode::Assemble()
        mov     rbp, QWORD PTR [rsp+8]
        mov     rsi, r12
        mov     rdi, rbp
        call    TNode::Assemble(TNode const*)
        mov     rbx, QWORD PTR [rsp+16]
        mov     rsi, r12
        mov     rdi, rbx
        call    TNode::Assemble(TNode const*)
        mov     rsi, rbp
        mov     rdi, r12
        call    TNode::Assemble(TNode const*)
        mov     rdi, rbp
        call    TNode::Assemble()
        mov     rsi, rbp
        mov     rdi, rbx
        call    TNode::Assemble(TNode const*)
        mov     rsi, rbx
        mov     rdi, r12
        call    TNode::Assemble(TNode const*)
        mov     rdi, rbp
        mov     rsi, rbx
        call    TNode::Assemble(TNode const*)
        add     rsp, 32
        mov     rdi, rbx
        pop     rbx
        pop     rbp
        pop     r12
        jmp     TNode::Assemble()
bar():
        push    r13
        push    r12
        push    rbp
        xor     ebp, ebp
        push    rbx
        sub     rsp, 40
.L9:
        mov     r13, QWORD PTR [rsp+rbp*8]
        xor     ebx, ebx
        lea     r12, [rbp+1]
.L5:
        cmp     rbp, rbx
        je      .L15
        mov     rsi, QWORD PTR [rsp+rbx*8]
        mov     rdi, r13
        add     rbx, 1
        call    TNode::Assemble(TNode const*)
        cmp     rbx, 3
        jne     .L5
        mov     rbp, r12
        cmp     r12, 3
        jne     .L9
.L16:
        add     rsp, 40
        pop     rbx
        pop     rbp
        pop     r12
        pop     r13
        ret
.L15:
        mov     rdi, r13
        mov     rbx, r12
        call    TNode::Assemble()
        cmp     r12, 3
        jne     .L5
        mov     rbp, r12
        cmp     r12, 3
        jne     .L9
        jmp     .L16

我看不懂汇编,但我似乎明白模板版本确实展开了循环,而bar有循环和分支。

然后我尝试使用 Clang++ (https://godbolt.org/z/VCNb65),我得到了一个非常不同的画面:

foo():                                # @foo()
        push    rax
        call    TNode::Assemble()
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble()
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        pop     rax
        jmp     TNode::Assemble()    # TAILCALL
bar():                                # @bar()
        push    rax
        call    TNode::Assemble()
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble()
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        call    TNode::Assemble(TNode const*)
        pop     rax
        jmp     TNode::Assemble()    # TAILCALL

这里发生了什么?生成的程序集怎么会这么简洁?

  1. NodePtr没有初始化,你用的时候就是UB。所以优化器可以为所欲为:这里它决定省略对寄存器 esi/rsi 的赋值,寄存器 esi/rsi 用于将参数传递给 TNode::Assemble(TNode const*) 和保存对象的 edi/rdi指针(this)。结果,您只看到一堆 call 指令。 尝试 value-initialize x(这将零初始化 NodePtr),

    T x{};
    

    你会得到更有意义的组装。

  2. Clang 似乎更擅长循环展开。参见,例如,。循环是否值得展开由您决定。对于小循环,它们可能是。但是你应该衡量一下。