我必须计算大量的3d向量,并且使用带有重载运算符+和运算符*的向量类与单独组件的总和的比较显示大约三倍的性能差异。我 know 假设差异必须是由于重载运算符中对象的构造。
如何避免构造并提高性能?
我特别感到困惑,因为以下基本上是afaik做标准的方法,我希望编译器能够优化它。在现实生活中,总和不会在一个循环中完成,而是在相当大的表达式中(总可执行的几十MB)总结不同的向量,这就是为什么在下面使用operator +。
class Vector
{
double x,y,z;
...
Vector&
Vector::operator+=(const Vector &v)
{
x += v.x;
y += v.y;
z += v.z;
return *this;
}
Vector
Vector::operator+(const Vector &v)
{
return Vector(*this) += v; // bad: construction and copy(?)
}
...
}
// comparison
double xx[N], yy[N], zz[N];
Vector vec[N];
// assume xx, yy, zz and vec are properly initialized
Vector sum(0,0,0);
for(int i = 0; i < N; ++i)
{
sum = sum + vec[i];
}
// this is a factor 3 faster than the above loop
double sumxx = 0;
double sumyy = 0;
double sumzz = 0;
for(int i = 0; i < N; ++i)
{
sumxx = sumxx + xx[i];
sumyy = sumyy + yy[i];
sumzz = sumzz + zz[i];
}
非常感谢任何帮助。
编辑: 谢谢大家的好评,我现在的表现处于同一水平。 @ Dima's特别是@ Xeo的回答就是这个伎俩。我希望我能够多次回答“接受”。我也会测试一些其他的建议。
答案 0 :(得分:7)
This文章对如何优化+
,-
,*
,/
等运算符进行了非常好的论证。
根据{{1}}:
operator+
实现为此类免费功能
operator+=
注意Vector operator+(Vector lhs, Vector const& rhs){
return lhs += rhs;
}
Vector是如何复制而不是引用。这允许编译器进行优化,例如复制省略
文章传达的一般规则:如果需要副本,请在参数中进行,因此编译器可以进行优化。本文不使用此示例,而是使用lhs
作为复制和交换习惯用法。
答案 1 :(得分:4)
为什么不替换
sum = sum + vec[i];
与
sum += vec[i];
...应该消除对复制构造函数的两次调用,并为每次迭代调用一次赋值运算符。
但与往常一样,简介并知道费用的来源而不是猜测。
答案 2 :(得分:4)
您可能对expression templates感兴趣。
答案 3 :(得分:2)
我实现了此处提出的大部分优化,并将其与函数调用(如
)的性能进行了比较Vector::isSumOf( Vector v1, Vector v2)
{
x = v1.x + v2.x;
...
}
对于交替顺序的每个方法,重复执行具有几十亿个向量求和的相同循环,不会产生承诺的增益。
如果bbtrb发布了成员函数,则此方法比isSumOf()
函数调用花费的时间多50%。
免费的非成员运算符+(Xeo)方法需要的时间是SumOf()
函数的两倍(100%以上)。
(gcc 4.6.3 -O3)
我意识到这一事实,这不是一个有代表性的测试,但因为我无法通过使用运算符来重现任何性能提升。如果可能的话,我建议避免使用它们。
答案 4 :(得分:1)
通常,operator +看起来像:
return Vector (x + v.x, y + v.y, z + v.z);
使用适当定义的构造函数。这允许编译器执行return value optimisation。
但如果您正在为IA32进行编译,那么SIMD将值得考虑,同时改进算法以利用SIMD特性。其他处理器可能具有SIMD样式指令。
答案 5 :(得分:1)
我认为性能上的差异是由编译器优化引起的。在循环中添加数组元素可以由编译器进行矢量化。现代CPU具有在单个时钟节拍中添加多个数字的指令,例如SSE,SSE2等。这似乎可能解释了您所看到的3个差异因素。
换句话说,在循环中添加两个数组的相应元素通常比添加类的相应成员更快。如果将向量表示为类中的数组,而不是x,y和z,则可以为重载运算符获得相同的加速。
答案 6 :(得分:1)
Vector操作符的实现是直接在头文件中运行还是在单独的cpp文件中?在头文件中,它们通常在优化的构建中内联。但如果它们是在不同的翻译单元中编译的,那么它们通常不会(取决于您的构建设置)。如果函数没有内联,那么编译器将无法进行您正在寻找的优化类型。
在这些情况下,请查看反汇编。即使你对汇编代码知之甚少,通常很容易弄清楚在这些简单情况下有什么不同。
答案 7 :(得分:1)
实际上,如果你查看任何真正的矩阵代码,运算符+和运算符+ =不会这样做。
由于涉及复制,它们将伪对象引入表达式,并且仅在执行赋值时执行实际工作。像这样使用延迟评估还允许在表达式评估期间删除NULL操作:
class Matrix;
class MatrixOp
{
public: virtual void DoOperation(Matrix& resultInHere) = 0;
};
class Matrix
{
public:
void operator=(MatrixOp* op)
{
// No copying has been done.
// You have built an operation tree.
// Now you are goign to evaluate the expression and put the
// result into *this
op->DoOperation(*this);
}
MatrixOp* operator+(Matrix& rhs) { return new MatrixOpPlus(*this,rhs);}
MatrixOp* operator+(MatrixOp* rhs){ return new MatrixOpPlus(*this,rhs);}
// etc
};
当然,这比我在这个简化示例中描述的要复杂得多。但是,如果您使用专为矩阵运算而设计的库,那么它已经为您完成了。
答案 8 :(得分:0)
像这样实施operator+()
:
Vector
Vector::operator+(const Vector &v)
{
return Vector(x + v.x, y + v.y, z + v.z);
}
并在类定义中添加inline
运算符(如果编译器发现它有用,这可以避免每个方法调用的返回地址和方法参数的堆栈推送和弹出)。
然后添加此构造函数:
Vector::Vector(const double &x, const double &y, const double &z)
: x(x), y(y), z(z)
{
}
可让您非常有效地构建新的向量(就像您在我的operator+()
建议中所做的那样)!
你做了:
for(int i = 0; i < N; ++i)
{
sum = sum + vec[i];
}
展开这种循环!在非常大的循环中仅执行一个操作(因为它将针对使用SSE2 / 3扩展或类似的东西进行优化)效率非常低。你应该做这样的事情:
//Unrolled loop:
for(int i = 0; i <= N - 10; i += 10)
{
sum = sum + vec[i];
+ vec[i+1];
+ vec[i+2];
+ vec[i+3];
+ vec[i+4];
+ vec[i+5];
+ vec[i+6];
+ vec[i+7];
+ vec[i+8];
+ vec[i+9];
}
//Doing the "rest":
for(int i = (N / 10) * 10; i < N; ++i)
{
sum = sum + vec[i];
}
(请注意,此代码未经测试,可能包含“off-by-one” - 错误左右......)
答案 9 :(得分:0)
请注意,您要求的是不同的东西,因为数据在内存中的排列方式不同。当使用向量数组时,坐标是交错的“x1,y1,z1,x2,y2,z2,...”,而对于双数组,你有“x1,x2,...,y1,y2,... z1” ,Z2 ......“。我想这可能会对编译器优化或缓存如何处理它产生影响。