复制构造函数 - C++:过度复制大型对象

copy constructor - c++: Excessive copying of large objects

本文关键字:复制 大型 对象 构造函数 C++      更新时间:2023-10-16

虽然在SO上已经有很多关于复制构造函数/赋值运算符的问题,但我没有找到适合我问题的答案。

我有一堂课,比如

class Foo
{
   // ...
private:
   std::vector<int> vec1;
   std::vector<int> vec2;
   boost::bimap<unsigned int, unsigned int> bimap;
   // And a couple more
};

现在似乎有一些相当多的复制正在进行(基于个人资料数据)。所以我的问题是如何最好地解决这个问题?

我应该实现自定义复制构造函数/赋值运算符并使用交换吗?或者我应该定义自己的交换方法并使用它(在适当的情况下)而不是赋值?

由于我不是 c++ 专家,因此非常感谢展示如何正确处理这种情况的示例。

更新:看来我不是很清楚。让我试着解释一下。该程序基本上是一个动态的广度优先搜索程序,对于采取的每个步骤,我需要存储有关该步骤的元数据(这是Foo类)。现在的问题是(通常)有指数级的步骤,所以你可以想象需要存储大量这些对象。据我所知,我总是通过(const)参考..每次我从图中的节点计算后继者时,我都需要创建并存储一个Foo对象(但是,一些数据成员将在处理此后继对象时进一步添加到此foo中)。

我的个人资料数据大致显示如下(我没有这台机器上的实际数字):

SearchStrategy::Search    13s
FooStore::Save            10s

所以你可以看到我花在保存元数据上的时间几乎和我在图表中搜索的时间一样多。哦,FooStore在google::sparse_hash_map<long long, Foo, boost::hash<long long> >中拯救了Foo

编译器是 g++4.4 或 g++4.5(我不在我的开发机器上,所以我目前无法检查)。

更新 2 我在构造后将一些成员分配给 Foo 实例,例如

void SetVec1(const std::vector<int>& vec1) { this->vec1 = vec1; };

我想明天,我应该将其更改为使用交换方法,这肯定会对此有所改善。

如果我不完全清楚我试图实现什么语义,我很抱歉,但原因是我不太确定。

问候

莫滕

一切都取决于复制此对象在您的情况下意味着什么:

  1. 这意味着复制它的全部价值
  2. 这意味着复制的对象将引用相同的内容

如果它是 1,那么这个类似乎是正确的。你不太清楚你说的操作确实会做很多副本,所以我假设你尝试复制整个对象。

如果它是 2,那么你需要使用 shared_ptr 之类的东西在对象之间共享容器。仅使用 shared_ptr 而不是真实对象作为成员将隐式地允许缓冲区由两个对象(副本和复制对象)引用。这是更简单的方法(如果您有一个启用 C++0x 的编译器提供它,请使用 boost::shared_ptr 或 std::shared_ptr)。

有更难的方法,但它们肯定会在以后成为一个问题。

  1. 当然,每个人都这么说,不要过早优化。不要为此烦恼,除非你证明a)你的程序运行得太慢,b)如果你没有复制那么多数据,它会跑得更快。

  2. 如果您的程序设计要求您同时保存数据的多个副本,则无能为力。您只需要咬紧牙关并复制数据。不,实现自定义复制构造函数和自定义赋值运算符不会使其运行得更快。

  3. 如果您的程序不需要此数据的多个同时副本,那么您确实有几个技巧可以减少您执行的副本数量。

检测复制方法 如果是我,即使在尝试改进任何东西之前,我还要做的第一件事就是计算我的复制方法的次数调用。

class Foo {
private:
  static int numberOfConstructors;
  static int numberofCopyConstructors;
  static int numberofAssignments;
  Foo() { ++numberOfConstructors; ...; }
  Foo(const Foo& f) : vec1(f.vec1), vec2(f.vec2), bimap(f.bimap) {
    ++numberOfCopyConstructors;
    ...;
  }
  Foo& operator=(const Foo& f) {
    ++numberOfAssignments;
    ...;
  }
};

运行程序时有和没有改进。打印出这些静态成员的值,以查看更改是否有任何效果。

通过使用引用避免函数调用中的赋值 如果将 Foo 类型的对象传递给函数,请考虑是否可以通过引用执行此操作。如果不更改传递的副本,则通过 const 引用传递它是不费吹灰之力的。

// WAS:
extern SomeFuncton(Foo f);
// EASY change -- if this compiles, you know that it is correct
extern SomeFunction(const Foo& f);
// HARD change -- you have to examine your code to see if this is safe
extern SomeFunction(Foo& f);

避免使用 Foo::swap 进行复制 如果经常使用复制方法(显式或隐式),请考虑分配自项是否可以放弃其数据,而不是复制它。

// Was:
vectorOfFoo.push_back(myFoo);
// maybe faster:
vectorOfFoo.push_back(Foo());
vectorOfFoo.back().swap(myFoo);
// Was:
newFoo = oldFoo;
// maybe faster
newfoo.swap(oldFoo);

当然,这只有在myFoooldFoo不再需要访问其数据时才有效。而且,您必须实施Foo::swap

void Foo::swap(Foo& old) {
    std::swap(this->vec1, old.vec1);
    std::swap(this->vec2, old.vec2);
    ...
}

无论您做什么,都要在更改之前之后衡量您的程序。测量调用复制方法的次数,以及程序中总时间的改进。

你的类看起来还不错,但你没有展示你如何使用它。

如果

有很多复制,则需要通过引用(或如果可能的话,常量引用)传递这些类的对象。如果必须复制该类,那么您什么都不能做。

如果这确实是一个问题,您可以考虑实现 pimpl 习惯用法。 但我怀疑这是一个问题,尽管我必须看到你对类的使用才能确定。

复制巨大的向量不太可能很便宜。最有希望的方法是复制更稀有。虽然C++无意调用 copy 非常容易(可能太容易),但有一些方法可以避免不必要的复制:

  • 通过常量和非常量引用传递
  • 移动构造函数
  • 具有所有权转让功能的智能指针

这些技术可能只留下算法所需的副本。

有时甚至可以避免其中一些复制。例如,如果您需要两个对象,其中第二个对象是第一个对象的反向副本,则可以创建一个包装器对象,其行为类似于反向,但不是存储整个副本只有一个引用。

减少复制的明显方法是使用类似shared_ptr的东西。然而,对于多线程,这种治疗方法可能比疾病更糟糕 - 增加和减少引用计数需要原子完成,这可能非常昂贵。但是,如果您通常最终修改了副本,并且需要每个副本都独一无二(即,修改副本不会影响原始副本),那么您最终可能会获得更差的性能,为引用计数支付原子增量/减少的费用,并且仍然做很多副本。

有几种明显的方法可以避免这种情况。一种是移动唯一的对象而不是复制 - 如果你能让它工作,那就太好了。另一种方法是大部分时间使用非原子引用计数,并且仅在线程之间移动数据时才进行深度复制。

不过,没有一个答案是普遍的,而且真的很干净。