问题描述
|
我正在创建一个多维矢量(数学矢量),其中允许基本的数学运算+,-,/,*,=。模板有两个参数,一个是类型(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];
};
在有人评论“ 1”之前,请注意,这是我的3D图形引擎的基础,并且必须快速。其次,我想了解一下自己的知识。
解决方法
您可以通过反汇编执行以下技巧,以查看特定代码的编译方式。
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
并查看disasm
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),524264(%rsp)
movdqa 524280(%rsp),%xmm0
paddd 524216(%rsp),524280(%rsp)
movdqa 524296(%rsp),%xmm0
paddd 524232(%rsp),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,(%rdx,%rax)
addq $16,%rax
cmpq $262144,%rax
jne .L2
#APP
# 38 \"1.cc\" 1
zzz
如您所见,第一个循环足够小,可以展开。第二个是循环。
,首先:现代CPU在预测分支方面非常聪明,因此展开循环可能无济于事(甚至可能会造成伤害)。
第二:是的,如果对您的目标CPU来说这是个好主意,现代的编译器就会知道如何展开这样的循环。
第三:现代的编译器甚至可以自动向量化循环,这甚至比展开更好。
底线:除非您对CPU架构了解很多,否则请不要认为自己比编译器更聪明。以简单,直接的方式编写代码,并且在分析器告诉您之前不要担心微优化。
,可以使用递归模板实例展开循环。在您的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];
};
,解决此问题的唯一方法是使用自己的优化参数在自己的编译器上进行尝试。使用您的“是否展开”代码(one6ѭ)制作一个测试文件:
#include \"myclass.hpp\"
void doSomething(Vector<double,3>& a,Vector<double,3>& b) {
a.add( b );
}
然后是参考代码段“ 8”:
#include \"myclass.hpp\"
void doSomething(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
会使它展开得更多。
,首先,完全不确定展开循环是否有益。
您问题的唯一可能答案是“取决于”(取决于编译器标志,取决于size
的值,等等)。
如果您真的想知道,请询问您的编译器:将其编译为汇编代码,其典型值为size
,并使用您实际使用的优化标志,然后检查结果。
,许多编译器都会展开此循环,不知道您指的“编译器”是否会。世界上不仅有一个编译器。
如果要保证已展开,则TMP(带有内联)可以做到这一点。 (这实际上是TMP的比较琐碎的应用程序之一,通常用作元编程的示例)。