【问题标题】:std::vector::reserve performance penaltystd::vector::reserve 性能损失
【发布时间】:2010-12-17 02:29:49
【问题描述】:
inline void add(const DataStruct& rhs) {
   using namespace boost::assign;
   vec.reserve(vec.size() + 3);
   vec += rhs.a, rhs.b, rhs.c;
}

上面的函数执行了大约 17000 次,它执行了(据我所见。其中涉及一些转换)调用 vector::reserve 时差了大约 2 个数量级。

我一直认为即使对于较小的值,reserve 也可以加快 push_back 的速度,但这似乎不是真的,我找不到任何明显的理由为什么不应该这样。保留会阻止函数的内联吗?对 size() 的调用是否太贵了?这取决于平台吗?我将尝试编写一些小型基准测试,以在干净的环境中确认这一点。

编译器:gcc (GCC) 4.4.2 with -g -O2

【问题讨论】:

  • 您是否尝试为 17000*3 输入预留空间?您的额外函数调用可能会产生一些开销,这可能会导致差异。
  • @splicer:这个数字是由于测试数据。实际调用次数是可变的。
  • 我的意思是,你所做的只有在处理更大的数字时才有效。 James Schek 的回答为您提供了一种通过可变数量的调用来完成此操作的方法,只要您知道开始的总数即可。否则你最好让默认实现为你处理它。
  • 你是在同一个类实例上调用 add() 吗?在这种情况下,每次添加都会使 vec 增加 3,而推回本身会使 vec 增长的频率要低得多。

标签: c++ performance stl stdvector


【解决方案1】:

只有在您事先知道要使用多少地方时才使用预留。

Reserve 需要复制整个向量...

如果你做一个push_back并且向量太小,那么它会做一个reserve (vec.size()*2)。

如果您事先不知道向量的大小以及需要随机访问,请考虑使用 std::deque。

【讨论】:

  • 这不是启发式方法。证明了平均一个新元素的插入是O(1)。阅读任何有关摊销分析的书籍(例如,算法简介)。
  • 好吧.. 我认为这是一种启发式算法,在数学上证明在平均情况下具有最佳性能。
  • 您可以通过乘以 3 而不是 2 来证明这一点。但如果它让你感到困扰,我还是会编辑它。
  • 我会说它既不是启发式也不是算法。这是一种适用于很多使用场景的策略。
  • 将当前分配的大小乘以任何常数(除了 0)将导致对数增长。改变常数只是调优,Big O 的性能还是一样的。 1.5 有利于较小的向量,2 有利于更大的向量。他们选择 2 是因为它在计算机领域是一个不错的整数,任何调整都将毫无意义,因为我的用法与您的不同。如果您需要调整行为,您可以使用 size() 和 reserve() 轻松颠覆默认行为。
【解决方案2】:

如果您事先知道元素的数量,则只能使用reserve()。在这种情况下,reserve() 同时为所有元素留出空间。

否则,只需使用 push_back() 并依赖默认策略 - 它将以指数方式重新分配并大大减少重新分配的数量,代价是内存消耗略微次优。

【讨论】:

    【解决方案3】:

    将保留移到添加之外。

    每次调用“add”时,都会保留至少 3 个额外元素。根据向量的实现,这可能几乎每次调用“add”时都会增加支持数组的大小。那肯定会导致您描述的性能差异。

    使用reserve的正确方法是这样的:

    vec.reserve(max*3);
    for(int i=0; i<max; i++)
       add(i);
    

    【讨论】:

      【解决方案4】:

      reserve() 的 GCC 实现将分配确切数量的元素,而 push_back() 将通过加倍内部缓冲区以指数方式增长,因此您正在击败指数增长并在每次迭代时强制重新分配/复制。在ltracevalgrind 下运行测试,查看malloc() 调用的数量。

      【讨论】:

      • +1。我刚刚检查了 GCC 的源代码,并打算写同样的。
      • 缓冲区通过 push_back() 的指数增长是否由标准或实现定义的保证?
      • 不,标准不保证指数增长,它是一个实现细节,与 reserve() 的这种特殊行为相同。
      • 标准有效地保证了指数增长,因为它是为 push_back 获得摊销 O(1) 行为的唯一合理方法(不合理的解决方案包括分配所有内存;))
      • 也可以解释 GCC 的行为:通过让您准确设置预期大小,在您知道实际需要多少内存的情况下,它们让您有机会不浪费内存。跨度>
      【解决方案5】:

      如果您对代码进行分析,我敢打赌,您会看到 += 非常快,问题是储备正在杀死您。当您对向量将增长到多大有所了解时,您实际上应该只使用储备。如果你能提前猜到,那就做一个预留,否则就用默认的push_back。

      【讨论】:

        【解决方案6】:

        当 std::vector 需要重新分配时,它的分配大小增加 N*2,其中 n 是它的当前大小。随着向量的增长,这会导致重新分配的对数数。

        如果 std::vector 将其分配的空间增加一个常数,那么重新分配的数量将随着向量的增长而线性增长。

        您所做的基本上是使向量以恒定量 3 增长,这意味着线性增长。线性显然比对数更糟糕,尤其是对于大数字。

        通常,唯一比对数更好的增长是恒定的。这就是标准委员会创建储备方法的原因。如果您可以避免所有重新分配(常量),您将比默认的对数行为表现更好。

        也就是说,您可能需要考虑 Herb Sutter 的 cmets 关于更喜欢 std::deque 而不是 vector www.gotw.ca/gotw/054.htm

        【讨论】:

          猜你喜欢
          • 2013-06-18
          • 2020-09-22
          • 2015-01-04
          • 1970-01-01
          • 2012-06-30
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多