高效的运营商+

时间:2011-02-18 20:17:31

标签: c++ operator-overloading

我必须计算大量的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的回答就是这个伎俩。我希望我能够多次回答“接受”。我也会测试一些其他的建议。

10 个答案:

答案 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)

您的Vector实现:

像这样实施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+()建议中所做的那样)!

在使用Vector的代码中:

你做了:

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 ......“。我想这可能会对编译器优化或缓存如何处理它产生影响。