简体   繁体   中英

How to reduce the + operator memory consumption for self-defined class in C++?

I give the following examples to illustrate my question:

 class BigClass
{
public:
    static int destruct_num;
    friend BigClass operator + (const BigClass &obj1, const BigClass &obj2);
    std::vector<int> abc;

    BigClass()
    {

    }

    ~BigClass()
    {
        destruct_num++;
        std::cout << "BigClass destructor " <<destruct_num<< std::endl;

    }

    BigClass(BigClass&& bc) :abc(std::move(bc.abc))
    {
        std::cout << "move operation is involed" << std::endl;
    }
};

int BigClass::destruct_num = 0;

BigClass operator + (const BigClass &obj1, const BigClass &obj2)
{
    BigClass temp;
    temp.abc = obj1.abc;
    temp.abc.insert(temp.abc.end(), obj2.abc.begin(), obj2.abc.end());

    return temp;

}

int main(void)
{

    BigClass a;
    a.abc = { 1,2,3 };
    BigClass b;
    b.abc = { 5,6,7 };
    BigClass c = a + b;

//  for (auto& v : c.abc)
//      std::cout << v << "  ";

    return 0;


}   

One problem with regard to operator + is that we have to generate a temp BigClass object temporally and then return it. Are there someways to reduce this burden?

Generally:

[...] the compilers are permitted, but not required to omit the copy [...]

Anyway your function should be optimised by any modern compiler because copy elision .

Here an example:

The result of the operator+ is in the assembly section:

call    operator+(BigClass const&, BigClass const&)
addl    $12, %esp

As you can see, no copy constructor is invoked in order to copy the result.

Indeed, if we disable the copy elision optimisation in GCC , the result changes:

call    operator+(BigClass const&, BigClass const&)
addl    $12, %esp
subl    $8, %esp
leal    -20(%ebp), %eax
pushl   %eax
leal    -56(%ebp), %eax
pushl   %eax
call    BigClass::BigClass(BigClass&&)
addl    $16, %esp
subl    $12, %esp
leal    -20(%ebp), %eax
pushl   %eax
call    BigClass::~BigClass()
addl    $16, %esp

After the call of operator+ the copy (or move in this case) constructor is called, and after the destructor of the temporary object.

Note that the copy elision is obtained even disabling optimisations ( -O0 ).

The same result is obtained with an older version: GCC 4.4.7 .


Since copy elision is not guaranteed for all architectures, you might implement some different solutions.

One possible solution is to avoid the allocation of a temporary variable inside the function, demanding the caller the reservation of that space. In order to do that, you should use a "custom" method and avoid to overload the operator+ .

void sum_bigClasses(const BigClass& obj1, const BigClass& obj2, BigClass& output) {
   // output.resize(obj1.size() + obj2.size());
   // std::copy(...);
}

Another solution it could be to implement a non-const operator for sum . An example:

BigClass& operator+=(const BigClass& rhs) {
   // std::copy(rhs.cbegin(), rsh.cend(), std::back_inserter(abc));
   return *this;
}

In this way the class interface allows different strategies:

  • Avoid to allocate 3 different object but only 2, if you don't need to preserve all different states.
  • Allow to allocate 3 different object and avoid temporary construction inside the operator.

Here, the two examples.

The first point:

BigClass o1;
BigClass o2;
// Fill o1 and o2;
o1 += o2;
// only 2 object are alive

The second point:

BigClass o1;
BigClass o2;
// Fill o1 and o2;
BigClass o3 = o1;  // Costructor from o1
o3 += o2;
// three different object

EDIT : Since the function it's a NRVO (the returned expression is not a prvalue ) neither the new standard C++17 will guarantee the copy elision.

If you run your code than you see that only 3 destructors are called. That means that the value of tmp object is moved, not copied, because of RVO (Return Value Optimization). The compiler doesn't copy it, because it sees that's not necessary.

The use of temporaries not only wastes memory but also processing time (calculating the sum of N BigClass instances may have quadratic time complexity in N). There is no general solution on avoiding this because it depends on how your objects are used. In this scenario:

BigClass c = a + b;

the compiler is already free (or required, C++17) to use copy elision, as explained by banana36, and the inputs are lvalues, therefore they cannot be changed without potentially causing great surprise.

A different scenario would be:

BigClass f();
BigClass g();

BigClass h = f() + g();

In this case, f() and g() are rvalues and copying both of them is wasteful. The storage of at least one of them could be reused, eg one could write an additional operator + overload to optimize the case where the left summand is an rvalue:

BigClass operator +(BigClass &&a, const BigClass &b)
{
    a.abc.insert(a.abc.end(), b.abc.begin(), b.abc.end());
    return std::move(a);
}

This reuses a.abc 's storage and avoids copying its contents as long as the capacity is sufficient. A nice side effect is that eg summing N objects with 10 elements each will have linear performance because insertion of a constant number of elements at the end of a std::vector has constant amortized cost. But it only works if the right overload of operator + is selected, which eg is not the case for std::accumulate . Here's an overview of your main options:

  1. Supply operator +(const BigClass &, const BigClass &) and operator += , and educate your users on the performance implications of using the former carelessly.
  2. Possibly add overloads for operator +(BigClass &&, const BigClass &) and maybe operator +(const BigClass &, BigClass &&) and operator +(BigClass &&, BigClass &&) . Note that if you have both overloads with one rvalue reference, you should absolutely also add the overload with two rvalue references, or else f() + g() will be an ambiguous call. Also note that the overload where the right hand parameter is an rvalue reference is best suited for use with eg std::deque and not std::vector because it has lesser time complexity on front insertion, but replacing a vector with a deque is only useful if this use case is common, because deque is otherwise slower than vector.
  3. Provide only efficient operations such as operator += and deal with the frustration of the users (alternatively, give the less efficient operations disparaging names like copyAdd ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM