繁体   English   中英

如何在C ++中减少自定义类的+运算符内存消耗?

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

我举几个例子来说明我的问题:

 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;


}   

关于operator +一个问题是我们必须暂时生成一个临时BigClass对象然后返回它。 有没有办法减轻这种负担?

通常:

[...]编译器是允许的,但不要求省略副本[...]

无论如何,你的函数应该由任何现代编译器优化,因为复制省略

是一个例子:

operator+的结果在汇编部分:

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

如您所见,为了复制结果,不会调用任何复制构造函数

实际上,如果我们在GCC中禁用复制省略优化, 结果会发生变化:

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

在调用operator+ ,复制(或在这种情况下移动) 构造函数被调用,并在临时对象的析构函数之后。

请注意,即使禁用优化( -O0 )也可以获得复制省略。

使用旧版本获得相同的结果: GCC 4.4.7


由于并非所有体系结构都保证了复制省略,因此您可以实现一些不同的解决方案。

一种可能的解决方案是避免在函数内部分配临时变量,要求调用者保留该空间。 为此,您应该使用“自定义”方法并避免使operator+过载。

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

另一种解决方案可能是为sum实现非const运算符。 一个例子:

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

通过这种方式,类接口允许不同的策略:

  • 如果您不需要保留所有不同的状态,请避免分配3个不同的对象,但只能分配2个。
  • 允许分配3个不同的对象,避免在运营商内部进行临时构建。

这里有两个例子。

第一点:

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

第二点:

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

编辑 :由于函数是NRVO (返回的表达式不是prvalue ),新的标准C ++ 17都不能保证复制省略。

如果运行代码而不是看到只调用了3个析构函数。 这意味着由于RVO(返回值优化),tmp对象的值被移动而不是复制。 编译器不会复制它,因为它认为没有必要。

临时使用不仅浪费内存而且浪费处理时间(计算N个BigClass实例的总和可能在N中具有二次时间复杂度)。 避免这种情况没有通用的解决方案,因为它取决于您的对象的使用方式。 在这种情况下:

BigClass c = a + b;

编译器已经免费(或需要,C ++ 17)使用复制省略,如banana36所解释,输入是左值,因此它们无法更改而不会引起很大的惊喜。

另一种情况是:

BigClass f();
BigClass g();

BigClass h = f() + g();

在这种情况下, f()g()是rvalues并且复制它们都是浪费。 可以重复使用它们中的至少一个的存储,例如,可以编写额外的operator +重载以优化左加数是右值的情况:

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

这会重复使用a.abc的存储空间,并且只要容量足够就可以避免复制其内容。 一个好的副作用是,例如,将每个具有10个元素的N个对象相加将具有线性性能,因为在std::vector的末尾插入恒定数量的元素具有恒定的摊销成本。 但它只有在选择了operator +的正确重载时才有效,例如std::accumulate 不是这种情况。 以下是您的主要选项概述:

  1. 提供operator +(const BigClass &, const BigClass &)operator += ,并教育用户不小心使用前者的性能影响。
  2. 可能为operator +(BigClass &&, const BigClass &)operator +(const BigClass &, BigClass &&)operator +(BigClass &&, BigClass &&)添加重载。 请注意,如果两个重载都带有一个右值引用,则绝对还应该使用两个右值引用添加重载,否则f() + g()将是一个模糊的调用。 另请注意,右手参数是右值参考的重载最适合与std::deque而不是std::vector因为它在前插入时具有较小的时间复杂度,但是仅使用双端队列替换向量如果此用例很常见,则有用,因为deque比vector更慢。
  3. 仅提供有效的操作,例如operator +=并处理用户的挫败感(或者,提供效率较低的操作贬低名称,如copyAdd )。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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