我正在创建一个多维向量(数学向量),其中我允许基本的数学运算+, - ,/,*,=。模板接受两个参数,一个是类型(int,float等),另一个是向量的大小。目前我通过for循环应用操作。现在考虑在编译时已知大小,编译器是否会展开循环?如果没有,有没有办法在没有(或最小)性能损失的情况下展开它?
template <typename T, u32 size>
class Vector
{
public:
// Various functions for mathematical operations.
// The functions take in a Vector<T, size>.
// Example:
void add(const Vector<T, size>& vec)
{
for (u32 i = 0; i < size; ++i)
{
values[i] += vec[i];
}
}
private:
T values[size];
};
在有人评论Profile then optimize
之前请注意,这是我的3D图形引擎的基础,它必须很快。其次,我想知道为了自我教育。
答案 0 :(得分:10)
您可以通过反汇编执行以下技巧,以查看特定代码的编译方式。
Vector<int, 16> a, b;
Vector<int, 65536> c, d;
asm("xxx"); // marker
a.Add(b);
asm("yyy"); // marker
c.Add(d);
asm("zzz"); // marker
现在编译
gcc -O3 1.cc -S -o 1.s
看到惨败
xxx
# 0 "" 2
#NO_APP
movdqa 524248(%rsp), %xmm0
leaq 524248(%rsp), %rsi
paddd 524184(%rsp), %xmm0
movdqa %xmm0, 524248(%rsp)
movdqa 524264(%rsp), %xmm0
paddd 524200(%rsp), %xmm0
movdqa %xmm0, 524264(%rsp)
movdqa 524280(%rsp), %xmm0
paddd 524216(%rsp), %xmm0
movdqa %xmm0, 524280(%rsp)
movdqa 524296(%rsp), %xmm0
paddd 524232(%rsp), %xmm0
movdqa %xmm0, 524296(%rsp)
#APP
# 36 "1.cc" 1
yyy
# 0 "" 2
#NO_APP
leaq 262040(%rsp), %rdx
leaq -104(%rsp), %rcx
xorl %eax, %eax
.p2align 4,,10
.p2align 3
.L2:
movdqa (%rcx,%rax), %xmm0
paddd (%rdx,%rax), %xmm0
movdqa %xmm0, (%rdx,%rax)
addq $16, %rax
cmpq $262144, %rax
jne .L2
#APP
# 38 "1.cc" 1
zzz
如您所见,第一个循环足够小,可以展开。第二个是循环。
答案 1 :(得分:4)
首先:现代CPU在预测分支方面非常聪明,因此展开循环可能没有帮助(甚至可能会受到伤害)。
第二:是的,现代编译器知道如何展开这样的循环,如果它对你的目标CPU来说是一个好主意。
第三:现代编译器甚至可以自动矢量化循环,这甚至比展开更好。
结论:除非你知道关于CPU架构的很多,否则不要认为你比编译器更聪明。以简单,直接的方式编写代码,在分析器告诉您之前不要担心微优化。
答案 2 :(得分:1)
首先,完全不确定展开循环是否有益。
你的问题唯一可能的答案是“它取决于”(关于编译器标志,size
的值等)。
如果您真的想知道,请询问您的编译器:使用典型值size
编译成汇编代码,并使用您用于实际的优化标记,并检查结果。
答案 3 :(得分:1)
解决这个问题的唯一方法是使用您自己的优化参数在您自己的编译器上进行尝试。使用“执行展开”代码生成一个测试文件test.cpp
:
#include "myclass.hpp"
void doSomething(Vector<double, 3>& a, Vector<double, 3>& b) {
a.add( b );
}
然后是参考代码段reference.cpp
:
#include "myclass.hpp"
void doSomething(Vector<double, 3>& a, Vector<double, 3>& b) {
a[0] += b[0];
a[1] += b[1];
a[2] += b[2];
}
现在使用GCC编译它们并只吐出汇编:
for x in *.cpp; do g++ -c "$x" -Wall -Wextra -O2 -S -o "out/$x.s"; done
根据我的经验,当使用在编译时已知持续时间的循环时,GCC默认会展开3或更少的循环;使用-funroll-loops
会导致它展开更多。
答案 4 :(得分:1)
可以使用递归模板实例化展开循环。这可能会或可能不会更快地实现C ++。
我稍微调整了你的例子,以便编译。
typedef unsigned u32; // or something similar
template <typename T, u32 size>
class Vector
{
// need to use an inner class, because member templates of an
// unspecialized template cannot be explicitly specialized.
template<typename Vec, u32 index>
struct Inner
{
static void add(const Vec& a, const Vec& b)
{
a.values[index] = b.values[index];
// triggers recursive instantiation of Inner
Inner<Vec, index-1>::add(a,b);
}
};
// this specialization terminates the recursion
template<typename Vec>
struct Inner<Vec, 0>
{
static void add(const Vec& a, const Vec& b)
{
a.values[0] = b.values[0];
}
};
public:
// PS! this function should probably take a
// _const_ Vector, because the argument is not modified
// Various functions for mathematical operations.
// The functions take in a Vector<T, size>.
// Example:
void add(Vector<T, size>& vec)
{
Inner<Vector, size-1>::add(*this, vec);
}
T values[size];
};
答案 5 :(得分:0)
许多编译器将展开此循环,不知道您所指的“编译器”是否会。世界上不只有一个编译器。
如果您想保证它已展开,那么TMP(带内联)可以做到这一点。 (这实际上是TMP的一个较为简单的应用,通常用作元编程的一个例子。)