我想知道是否值得通过模板帮助编译器展开一个简单的循环。我准备了以下测试:
#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
这里发生了什么?产生的装配如何如此简洁?
答案 0 :(得分:2)
NodePtr
未初始化,使用时为UB。因此优化器可以做任何想做的事情:在这里,它决定省略对寄存器esi/rsi
的赋值,该赋值用于将参数传递给TNode::Assemble(TNode const*)
和edi/rdi
,后者保存一个对象指针(this
)。结果,您只会看到一堆call
指令。
尝试value-initialize x
(这将对NodePtr
进行零初始化),
T x{};
您将获得更有意义的汇编。
Clang在循环展开方面似乎更好。参见例如this answer。由您决定循环是否值得展开。对于小循环,可能是这样。但是你应该测量。