【问题标题】:Loop unrolling - G++ vs. Clang++循环展开 - G++ 与 Clang++
【发布时间】:2018-10-30 07:06:00
【问题描述】:

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

#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

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

【问题讨论】:

    标签: c++ g++ c++17 clang++ loop-unrolling


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

      T x{};
      

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-09-18
      • 2011-07-23
      • 1970-01-01
      • 1970-01-01
      • 2018-11-01
      • 2016-08-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多