如果我想要最大速度,我应该在std :: vector上使用数组吗?

时间:2013-04-13 20:00:44

标签: c++ arrays performance vector std

我正在编写一些代码,需要尽可能快地完成所有研究时间(换句话说,没有手动优化装配)。

我的系统主要由一堆3D点(原子系统)组成,因此我编写的代码进行了大量的距离比较,最近邻搜索以及其他类型的排序和比较。这些是大型,百万或十亿点系统,并且嵌套用于循环的朴素O(n ^ 2)不会削减它。

我最简单的方法是使用std::vector来保持点坐标。起初我认为它可能与阵列一样快,所以这太棒了!然而,这个问题(Is std::vector so much slower than plain arrays?)让我感到非常不安。我没有时间使用数组和向量编写所有代码并对它们进行基准测试,所以我现在需要做出一个好的决定。

我相信知道std::vector后面的详细实现的人可以使用这些功能而且速度很小。但是,我主要用C语言编程,所以我不知道std::vector在幕后做了什么,我不知道push_back每次调用它时是否会执行一些新的内存分配或者我可能陷入的其他“陷阱”使我的代码变得非常慢。

虽然阵列很简单;我确切地知道什么时候分配内存,我所有算法的顺序是什么,等等。我可能不得不忍受blackbox未知数。然而,我常常看到人们批评在互联网上使用数组而不是向量,我不禁想知道我是否缺少更多信息。

编辑:为了澄清,有人问“你为什么要用数组或向量来操纵这么大的数据集”?好吧,最终,所有内容都存储在内存中,因此您需要选择一些底层抽象。例如,我使用kd-tree来保存3D点,但即便如此,kd-tree也需要通过数组或向量构建。

另外,我并不是说编译器无法优化(我知道最好的编译器在许多情况下可以胜过人类),但仅仅是因为它们不能优于其约束允许的优化,而且我可能会无意中引入约束条件我对矢量的实现一无所知。

7 个答案:

答案 0 :(得分:2)

所有这些都取决于你如何实现你的算法。 std::vector是一种通用的容器概念,它为我们提供了灵活性,但却让我们有意识地构建算法实现的自由和责任。我们将从std::vector观察到的大部分效率开销来自复制std::vector提供了一个构造函数,它允许您使用值X初始化N个元素,当您使用它时,向量与数组一样快。

我做了一个测试std::vector与数组描述here

#include <cstdlib>
#include <vector>

#include <iostream>
#include <string>

#include <boost/date_time/posix_time/ptime.hpp>
#include <boost/date_time/microsec_time_clock.hpp>

class TestTimer
{
public:
    TestTimer(const std::string & name) : name(name),
        start(boost::date_time::microsec_clock<boost::posix_time::ptime>::local_time())
    {
    }

    ~TestTimer()
    {
        using namespace std;
        using namespace boost;

        posix_time::ptime now(date_time::microsec_clock<posix_time::ptime>::local_time());
        posix_time::time_duration d = now - start;

        cout << name << " completed in " << d.total_milliseconds() / 1000.0 <<
            " seconds" << endl;
    }

private:
    std::string name;
    boost::posix_time::ptime start;
};

struct Pixel
{
    Pixel()
    {
    }

    Pixel(unsigned char r, unsigned char g, unsigned char b) : r(r), g(g), b(b)
    {
    }


    unsigned char r, g, b;
};

void UseVector()
{
    TestTimer t("UseVector");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
        pixels.resize(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }
    }
}

void UseVectorPushBack()
{
    TestTimer t("UseVectorPushBack");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels;
            pixels.reserve(dimension * dimension);

        for(int i = 0; i < dimension * dimension; ++i)
            pixels.push_back(Pixel(255, 0, 0));
    }
}

void UseArray()
{
    TestTimer t("UseArray");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        Pixel * pixels = (Pixel *)malloc(sizeof(Pixel) * dimension * dimension);

        for(int i = 0 ; i < dimension * dimension; ++i)
        {
            pixels[i].r = 255;
            pixels[i].g = 0;
            pixels[i].b = 0;
        }

        free(pixels);
    }
}
void UseVectorCtor()
{
    TestTimer t("UseConstructor");

    for(int i = 0; i < 1000; ++i)
    {
        int dimension = 999;

        std::vector<Pixel> pixels(dimension * dimension, Pixel(255, 0, 0));
    }
}

int main()
{
    TestTimer t1("The whole thing");

    UseArray();
    UseVector();
    UseVectorCtor();
    UseVectorPushBack();

    return 0;
}

这里是结果(在Ubuntu amd64上用g ++ -O3编译):

  

UseArray在 0.325 秒内完成   UseVector在1.23秒内完成
  UseConstructor在0.866秒完成
  UseVectorPushBack在8.987秒完成
  整件事在11.411秒内完成

显然push_back在这里不是一个好选择,使用构造函数仍然比数组慢2倍。 现在,为Pixel提供空副本Ctor:

Pixel(const Pixel&) {}

给出了以下结果:

  

UseArray在 0.331 秒内完成   UseVector在0.306秒内完成
  UseConstructor在0秒内完成
  UseVectorPushBack在2.714秒完成
  整件事在3.352秒内完成

总而言之:重新思考你的算法,否则,可能需要在New [] / Delete []周围使用自定义包装器。在任何情况下,由于某些未知原因,STL实现并不慢,它只是完全按照你的要求执行;希望你更清楚。 在刚开始使用向量的情况下,它们的行为可能会令人惊讶,例如这段代码:

class U{
    int i_;
public:
    U(){}
    U(int i) : i_(i) {cout << "consting " << i_ << endl;}
    U(const U& ot) : i_(ot.i_) {cout << "copying " << i_ << endl;}
};

int main(int argc, char** argv)
{   
    std::vector<U> arr(2,U(3));
    arr.resize(4);
    return 0;
}

结果:

  

consting 3

     

复制3

     

复制3

     

复制548789016

     

复制548789016

     

复制3

     

复制3

答案 1 :(得分:1)

向量保证底层数据是内存中的连续块。保证这一点的唯一理智方法是将其实现为数组。

推送新元素时的内存重新分配可能会发生,因为向量无法预先知道要添加多少元素。但是,如果事先知道,可以使用适当数量的条目调用reserve,以避免在添加时重新分配。

向量通常比数组更受欢迎,因为它们在使用.at()访问元素时允许进行边界检查。这意味着访问向量之外的索引不会像数组中那样导致未定义的行为。但是,这种绑定检查确实需要额外的CPU周期。使用[]-operator访问元素时,不会进行边界检查,访问速度应与数组一样快。但是,当您的代码出错时,这会冒未定义的行为。

答案 2 :(得分:1)

发明STL然后进入C ++标准库的人是 咒骂删除 smart。甚至不要让自己想象一下,由于您对传统C阵列的卓越知识,您可以超越它们。 (如果你认识一些Fortran,你将有机会。)

使用std::vector,您可以一次性分配所有内存,就像使用C数组一样。您也可以再次分配,就像使用C数组一样。您可以控制每次分配的时间,就像使用C数组一样。 与C数组不同,您也可以忘记这一切,让系统为您管理分配,如果这是您想要的。这是绝对必要的基本功能。我不确定为什么有人会认为它丢失了。

说了这么多,如果你发现它们更容易理解,请使用数组。

答案 3 :(得分:1)

我并不是真的建议你去寻找阵列或载体,因为我认为根据你的需要,它们可能并不完全合适。

您需要能够有效地组织数据,以便查询不需要扫描整个内存范围以获取相关数据。因此,您希望将更可能一起选择的点组合在一起。

如果您的数据集是静态的,那么您可以脱机进行排序,并使您的数组在应用程序启动时加载到内存中,并且向量或数组可以正常工作(前提是您执行{ {1}}预先调用reserve,因为默认的分配增长方案会在底层数组满了时将其大小加倍,并且您不希望仅使用16Gb的内存来获得9Gb的值数据)。

但是如果您的数据集是动态的,那么使用向量或数组在集合中进行高效插入将很困难。回想一下,数组中的每个插入都会创建一个地方的所有后继元素的移位。当然,索引(如您提到的kd树)将有助于避免对阵列进行全面扫描,但如果所选点分散在整个阵列中,则对内存和缓存的影响基本相同。这种转变也意味着需要更新指数。

我的解决方案是在页面中切割数组(列表链接或数组索引)并在页面中存储数据。这样,就可以将相关元素组合在一起,同时仍然保持页面内连续存储器访问的速度。然后索引将引用该页面中的页面和偏移量。页面不会自动填充,这会留下插入相关元素的空间,或者使轮班变得非常便宜。

请注意,如果页面总是满的(除了最后一页),在插入时仍然需要移动每一页,而如果允许不完整的页面,则可以限制转换到单页,如果该页面已满,请在其后面插入一个新页面以包含补充元素。

要记住的一些事情:

  • 数组和向量分配具有上限,这取决于OS(这些限制可能不同)

在我的32位系统上,3D点矢量的最大允许分配大约为1.8亿个条目,因此对于较大的数据集,必须找到不同的解决方案。当然,在64位操作系统上,该数量可能会大得多(在Windows 32位上,进程的最大内存空间为2Gb - 我认为他们在更高级版本的操作系统上添加了一些技巧来扩展该数量)。不可否认,对于像我这样的解决方案,记忆会更加成问题。

  • 调整向量大小需要分配堆的新大小,将元素从旧内存块复制到新内存块。

因此,为了仅向序列中添加一个元素,在调整大小期间需要两倍的内存。此问题可能无法提供普通数组,可以使用ad hoc OS内存函数(例如,vector)重新分配,但据我所知,该函数不能保证相同的内存chunck将被重用)。如果使用使用相同功能的自定义分配器,则可以在向量中避免该问题。

  • C ++没有对底层内存架构做出任何假设。

向量和数组用于表示由分配器提供的连续内存块,并将该内存块用接口包装以访问它。但是C ++并不知道操作系统是如何管理内存的。在大多数现代操作系统中,该内存实际上是以页面形式切割的,这些页面被映射到物理内存中或从物理内所以我的解决方案是以某种方式在流程级别重现该机制。为了使分页有效,有必要让我们的页面适合OS页面,因此需要一些与操作系统相关的代码。另一方面,对于基于矢量或阵列的解决方案,这根本不是问题。

所以从本质上讲,我的答案是关注更新数据集的效率,这种方式有利于聚类点彼此接近。它假设这种聚类是可能的。如果不是这样的话,那么只需在数据集末尾推一个新点就可以了。

答案 4 :(得分:0)

虽然我不知道std:vector的确切实现,但是大多数这样的列表系统比数组慢,因为它们在调整大小时分配内存,通常是当前容量的两倍,尽管情况并非总是这样。

因此,如果向量包含16个项目并且您添加了另一个项目,则需要另外16个项目的内存。由于向量在内存中是连续的,这意味着它将为32个项目分配一个固体内存块并更新向量。您可以通过构造std:vector来获得一些性能改进,其初始容量大约是您认为数据集的大小,尽管这并不总是很容易得到的数字。

答案 5 :(得分:0)

对于矢量和数组之间常见的操作(因此不是push_back或pop_back,因为数组的大小是固定的),它们的执行完全相同,因为 - 通过规范 - 它们是相同的。

向量访问方法是如此微不足道,以至于更简单的编译器优化将消除它们。 如果您事先知道了矢量的大小,只需通过指定大小来构建它,或者只需调用resize,就可以获得与new []相同的效果。

如果您不知道大小,但是您知道需要增长多少,只需调用reserve,并且在push_back上没有任何问题,因为已经分配了所有必需的内存。

在任何情况下,搬迁都不是那么“愚蠢”:矢量的容量和大小是两个不同的东西,容量通常在耗尽时加倍,因此大量的重新定位越来越少。< / p>

此外,如果您了解有关尺寸的所有信息,并且您不需要动态内存并且需要相同的矢量界面,请同时考虑std::array

答案 6 :(得分:0)

听起来你需要一些内存,所以你不需要分页。我倾向于赞同@ Philipp的回答,因为你真的很想确保它不会在引擎盖下重新分配

这是一棵需要重新平衡的树吗? 你甚至思考关于编译器优化?

请参加如何优化软件的速成课程。 我相信你对Big-O一无所知,但我敢打赌你习惯于忽略常数因素,对吧?他们可能会被打破2至3个数量级,做一些你从未想过的事情。 如果这转化为计算时间的天数,也许它会变得有趣。

并且没有编译器优化器可以为您解决这些问题。

如果您有学术倾向, this post 会详细介绍。