问题 push_back用于向量,双端队列和列表


我正在尝试优化C ++例程。这个例程的主要瓶颈是对象向量的push_back()。我尝试使用deque而不是尝试了一个列表。但奇怪的是(与理论相反)deque和list实现比vector对应运行得慢得多。

事实上,甚至clear()对于deque和list实现来说比向量对应运行要慢得多。在这种情况下,Vector实现似乎是最快的,而列表实现是最慢的。

有什么指针吗?

注意:vector reserve()可以加速实现但不能完成,因为它的大小未知。

谢谢。


5591
2018-03-27 22:52


起源

另一个注意事项:push_back的结果类似,矢量最快,列表最慢。 - Vidya
你想要推回什么?复制是否昂贵?它有一个昂贵的复制构造函数吗?发布更多细节。 - Brian Neal
如果复制很昂贵且你有一个“交换”功能,你可以避免一些副本(见我的回答) - Rexxar
确保您在发布模式下进行配置。根据编译器,比较Debug向量和deque几乎没有任何意义。 - Constantin
这是预期的,因为向量和列表的n个项的push_backs是O(n),但是向量具有更简单的结构(简单的数组),因此对push_back的操作更少。但当然,插入将是不同的故事。 - Tae-Sung Shin


答案:


可以预期矢量比deque或list更快建立或清除;这是一个更简单的数据结构。

关于至于对于有关 vector::push_back,它必须做两件事:

  1. 检查向量是否足够大 持有新项目。
  2. 插入新项目。

通常只需调整矢量大小并使用即可消除步骤1,从而加快速度 operator[] 设置项目。

更新:  原始海报问了一个例子。 下面的代码乘以128兆插入和输出

push_back           : 2.04s
reserve & push_back : 1.73s
resize & place      : 0.48s

在旧的P4机器上使用g ++ -O3在Debian / Lenny上编译和运行时。

#include <iostream>
#include <time.h>
#include <vector>

int main(int,char**)
{
  const size_t n=(128<<20);

  const clock_t t0=clock();
  {
    std::vector<unsigned char> a;
    for (size_t i=0;i<n;i++) a.push_back(i);
  }
  const clock_t t1=clock();
  {
    std::vector<unsigned char> a;
    a.reserve(n);
    for (size_t i=0;i<n;i++) a.push_back(i);
  }
  const clock_t t2=clock();
  {
    std::vector<unsigned char> a;
    a.resize(n);
    for (size_t i=0;i<n;i++) a[i]=i;
  }
  const clock_t t3=clock();

  std::cout << "push_back           : " << (t1-t0)/static_cast<float>(CLOCKS_PER_SEC) << "s" << std::endl;
  std::cout << "reserve & push_back : " << (t2-t1)/static_cast<float>(CLOCKS_PER_SEC) << "s" << std::endl;
  std::cout << "resize & place      : " << (t3-t2)/static_cast<float>(CLOCKS_PER_SEC) << "s" << std::endl;

  return 0;  
}

6
2018-03-27 23:07



你能给我一个相同的例子吗?谢谢。 - Vidya
更简单并不总是意味着更快。向量可能很简单,但对于非顺序非int键查找(几乎在所有情况下),哈希映射更快。 - strager
为什么reserve / push_back比调整大小/位置慢得多?认为储备是为了这个特殊目的...... - Cray
@Cray:即使你保留,每个push_back仍在检查容量是否还没有超过(有点重复在驱动循环中进行的比较,但是编译器不够聪明,无法计算出来) 。听起来微不足道,但当你做了数以千万计的时候,总量是很重要的。 “调整大小和放置”版本避免了此检查的开销。 - timday


如果您不知道要添加多少个对象,则很难找到最佳解决方案。您所能做的就是尽量减少您所知道的成本 - 在这种情况下,您的矢量会不断调整大小。

你可以用两种方式做到这一点;

1)将您的操作分解为构建和完成。在这里,您可以将列表构建为一个保证足够大的向量,并在完成后将其复制到另一个向量。

例如。

std::vector<Foo> hugeVec;
hugeVec.reserve(1000);    // enough for 1000 foo's

// add stuff

std::vector<Foo> finalVec;
finalVec = hugeVec;

2)或者,当你的向量是完整的呼叫保留时,足够用于另一组对象;

if (vec.capacity() == vec.size())
  vec.reserve(vec.size() + 16);  // alloc space for 16 more objects

您可以选择一个不同的容器,该容器不会导致在调整大小时复制所有元素,但您的瓶颈可能会成为新元素的单独内存分配。


3
2018-03-27 23:12



你最好以指数方式增加储备而不是线性(vec.reserve(vec.size()* 2)),它会给你更好的平均性能。 - Eclipse
指数增长的问题在于它变得非常快得多:)不知道涉及多少项目,或者每个项目的大小,我选择了一个更安全的例子,但是,指数可能是一个更好的选择。 - Andrew Grant
我认为你最好调用finalVec.swap(hugeVec)来避免赋值运算符中不必要的副本。 - Idan K
关于指数式增长:它只是对应用程序所需内容的启发式方法。而且非常好。由于推送是瓶颈,这意味着 负载 对于扩展,如果内存无法接受,则需要更多内存。 - xtofl
但是,分享“构建”和“最终确定”是个好主意。 - xtofl


你是在推回物体本身还是指向它们的指针?与无论对象的大小相比,指针通常要快得多,因为复制只需要4-8个字节。


2
2018-03-27 22:56



是的,我正在推回物体。我会尝试指针。谢谢。 - Vidya
不复制对象的数据...调用复制构造函数。在某些情况下,这比普通的memcpy慢。 - strager


如果对象的副本很慢,“push_back()”可能会很慢。如果默认构造函数很快并且您有一种方法可以使用swap来避免复制,那么您可以使用更快的程序。

void test_vector1()
{
    vector<vector<int> > vvi;
    for(size_t i=0; i<100; i++)
    {
        vector<int> vi(100000, 5);
        vvi.push_back(vi);    // copy of a large object
    }
}

void test_vector2()
{
    vector<int> vi0;
    vector<vector<int> > vvi;
    for(size_t i=0; i<100; i++)
    {
        vector<int> vi(100000, 5);
        vvi.push_back(vi0);  // copy of a small object
        vvi.back().swap(vi); // swap is fast
    }
}

结果:

VS2005-debug 
* test_vector1 -> 297
* test_vector2 -> 172

VS2005-release
* test_vector1 -> 203
* test_vector2 -> 94

gcc
* test_vector1 -> 343
* test_vector2 -> 188

gcc -O2
* test_vector1 -> 250
* test_vector2 -> 156

2
2018-03-28 14:40



在test_vector2中,只需在循环之前执行vvi.resize(100)并删除vvi.push_back(vi0)。我打赌你会获得相当大的加速。 - Constantin
@Constantin:我使用过“push_back”,因为Vidya说他的矢量大小在他的程序中是未知的。 - Rexxar


如果你想要矢量快,你必须保留()足够的空间。它产生了巨大的差异,因为每次成长都是非常昂贵的。如果你不知道,做个好猜。


1
2018-03-27 23:15





您需要提供有关例程行为的更多信息。

在一个地方,你关心的速度 push_back() 在另一个你关心的问题 clear()。你在建造集装箱,做什么然后倾倒它?

你看到的结果 clear() 是因为 vector<>只需释放单块内存, deque<> 必须发布几个,和 list<> 必须为每个元素释放一个。


0
2018-03-27 22:58



push_back()的结果也是类似的,vector是最快的,list是最慢的。我想要优化的主程序是一个具有for循环(计数为1亿或更多)的程序,其中一个对象被推回。其父例程清除向量。 - Vidya
重新使用vector :: reserve() - 您是否有合理的猜测可以添加多少元素?执行保留(some_decent_guess)应该减少发生的重新分配/复制周期的数量,并且只要你没有保留大量的数据就不应该伤害任何东西。 - Michael Burr


Deque的结构比矢量更复杂,两者之间的速度差异将在很大程度上取决于特定的实现和推回的元素的实际数量,但对于大量数据,它应该更快。 clear()可能会更慢,因为它可能会选择摆脱更复杂的底层结构。列表大致相同。


0
2018-03-27 23:05





关于push_back()很慢并且保留没有帮助,MSVC中使用的STL的实现类似于:当你第一次创建一个向量时,它为我认为10个元素保留了空间。从那时起,每当它变满时,它就会保留1.5倍于向量中元素数量的空间。所以,像10,15,22,33,49,73,105,157 ......重新分配是昂贵的。

即使您不知道确切的大小,reserve()也很有用。如果需要,reserve()不会阻止向量增长。如果你保留()并且向量增长超过那个大小,你仍然会因为保留而改进了东西。如果向量变得更小,那么,也许这没关系,因为一般情况下,性能越好,尺寸越小。

您需要在RELEASE模式下进行分析,以确定哪种策略最有效。


0
2018-03-27 23:12



+1发布模式。在MSVC中调试模式下的STL非常慢(但检查得很好)。 - Eclipse


你必须根据你将要做的事情选择你的容器。

相关行动是:延长(与 push),插入(可能根本不需要),提取,删除。

cplusplus.com,每个容器类型的操作有一个非常好的概述。

如果操作是 push-bound,向量击败所有其他向量是有意义的。 deque的好处在于它分配了固定的块,因此可以更有效地使用碎片内存。


0
2018-03-29 07:40