问题 选择性能最佳的容器(阵列)


这是关于容器的一个小问题,特别是数组。

我正在写一个物理代码,主要操纵一个大的(> 1 000 000)一组“粒子”(6 double 每个坐标)。我正在寻找最好的方法(在性能方面)来实现一个类,它将包含这些数据的容器,并将为这些数据提供操作原语(例如,实例化, operator[]等)。

如何使用此集合有一些限制:

  • 它的大小是从配置文件中读取的,在执行期间不会更改
  • 它可以被视为N(例如1 000 000)行和6列(每个存储一维坐标)的大二维数组
  • 数组在一个大循环中操作,访问每个“粒子/线”并使用其坐标进行计算,并将结果存储回该粒子,依此类推每个粒子,依此类推,每次迭代大循环。
  • 在执行期间不添加或删除新元素

第一个结论,因为对元素的访问基本上是通过逐个访问每个元素来完成的 [],我认为我应该使用普通的动态数组。

我已经探讨了一些事情,我想对那些可以给我最好的表现的人发表意见。

据我所知,使用动态分配的数组而不是a是没有优势的 std::vector,所以像 double** array2d = new ..., loop of new, etc 被排除在外。

所以使用它是个好主意 std::vector<double> ?

如果我使用 std::vector,我应该创建一个像二维数组 std::vector<std::vector<double> > my_array 可以像索引一样索引 my_array[i][j],或者这是一个坏主意,它会更好用 std::vector<double> other_array 并使用 other_array[6*i+j]

也许这可以提供更好的性能,特别是当列的数量是固定的并且从一开始就知道。

如果您认为这是最佳选择,是否可以将此向量包装为可以使用定义为的索引运算符进行访问 other_array[i,j] // same as other_array[6*i+j] 没有开销(如每次访问时的函数调用)?

另一种选择,我到目前为止使用的是特别使用闪电战 blitz::Array

typedef blitz::Array<double,TWO_DIMENSIONS> store_t;
store_t my_store;

在哪里访问我的元素: my_store(line, column);

我认为在我的情况下使用Blitz没有太大的优势,因为我逐个访问每个元素,如果我直接在数组上使用操作(如矩阵乘法),那么Blitz会很有趣,我不是。

你认为闪电战是好的,还是在我的情况下没用?

这些是我到目前为止考虑的可能性,但也许是最好的一个我还是另一个,所以不要犹豫,建议我其他的东西。

非常感谢您对此问题的帮助!

编辑:

从非常有趣的答案和评论下面看来,一个好的解决方案如下:

  • 使用结构 particle (包含6个双打)或6个双打的静态数组(这避免使用二维动态数组)
  • 用一个 vector 或者a deque 这个的 particle 结构或数组。然后用迭代器遍历它们是很好的,这将允许稍后从一个变为另一个。

另外我也可以使用 Blitz::TinyVector<double,6> 而不是一个结构。


9887
2017-08-31 08:15


起源

只是好奇:你在建模什么样的物理学? - flies
你可以看看这个 ab-project-mte.web.cern.ch/AB-Project-MTE, 和这个 ab-project-mte.web.cern.ch/AB-Project-MTE/Documentation/Movies/... - Cedric H.


答案:


首先,你不想在一个地方散布一个给定粒子的坐标,所以我首先要写一个简单的 struct

struct Particle { /* coords */ };

然后我们可以制作一个简单的一维数组 Particles

我可能会用一个 deque,因为这是默认容器,但您可能希望尝试一下 vector,只有1.000.000的粒子意味着大约几个MB的单个块。它应该成立,但如果这种情况有所增长,它可能会使你的系统紧张 deque 将分配几个块。

警告

正如亚历山大·C所说,如果你去了 deque 道路,不要使用 operator[] 并且更喜欢使用迭代风格。如果你真的需要随机访问并且它的性能敏感,那么 vector 应该证明更快。


4
2017-08-31 08:23



岂不 vector 更好,因为我们可以保留使用的内存量 reserve() 以便以后不进行分配? - Naveen
但 请 不要在循环中使用带[deque]的operator [] - Alexandre C.
@jdv:你当然可以在适当的地方修改它(除了 set)! @Alexandre C:很好的警告,有一个 deque最好使用迭代器。 - Matthieu M.
@Cedric:deque不会分配一个块。如果你不需要随机访问,那就更好了:你可以轻松地替换你先尝试的任何容器并测量差异。并尝试 std::vector 第一。 - sbi
@Cedric H: deque 允许在O(1)的两端添加/删除元素,同时插入a的前面 vector 导致元素被洗牌(这对于小容器来说无关紧要)。你可以想到一个 deque 作为元素数组的队列。 - Matthieu M.


所以使用它是个好主意 std::vector<double> ?

通常,a std::vector 应该是容器的首选。你也可以使用 std::vector<>::reserve() 要么 std::vector<>::resize() 在填充向量时避免重新分配。是否有更好的其他容器可以找到 测量。而且只能通过测量。但首先要衡量容器所涉及的内容(填充,访问元素)是否值得优化。

如果我使用std :: vector,我应该创建一个二维数组 std::vector<std::vector<double> > [...]

编号IIUC,您每个粒子访问您的数据,而不是每行。如果是这样的话,为什么不使用 std::vector<particle>,哪里 particle 是一个持有六个值的结构?即使我理解不正确,你也应该在一维容器周围写一个二维包装器。然后按行或列对齐数据 - 访问模式的速度会更快。

你认为闪电战是好的,还是在我的情况下没用?

我没有关于blitz ++及其所用区域的实用知识。但是,并不是blitz ++所有关于表达模板来展开循环操作并在进行矩阵操作时优化临时工作? ICBWT。


8
2017-08-31 08:29



好的 particle 结构体。关于闪电战:实际上我能做的就是用一个 particle 结构,实际上是一个blitz :: Array,而不是通过坐标操作执行坐标,然后我可以直接将它们作为向量进行操作,然后我可以更有效地执行。也许... - Cedric H.
尼尔巴特沃思有一个非常有趣的博客文章,关于为什么 deque 应该是默认容器 vector 被选为C兼容性。然而,因为他要求删除他在SO上的帐户,我不记得博客的地址...我很难指出你。 - Matthieu M.
@Matthieu:它在某处 punchlet.wordpress.com? - sbi
@sbi:是的!我马上给这个书签,这是文章: punchlet.wordpress.com/2009/12/27/letter-the-fourth 它开始于对CS类中我们如何被教导列表的咆哮,当它是关于可以找到的效率最低的容器时。 - Matthieu M.
@sbi:你是对的,我为一个错误的记忆而道歉,而不是自己重新阅读这篇文章。 - Matthieu M.


从容器中选择时的第一条规则是使用 std::vector。然后,只有在您的代码完成并且您可以实际测量性能之后,您才可以尝试其他容器。但首先坚持矢量。 (并使用 reserve() 从头开始)

然后,你不应该使用 std::vector<std::vector<double> >。你知道数据的大小:它是6倍。不需要它是动态的。它是固定不变的。你可以定义一个结构来保存粒子成员(六个双打),或者你可以简单地输入它: typedef double particle[6]。然后,使用粒子矢量: std::vector<particle>

此外,由于程序按顺序使用向量中包含的粒子数据,因此您将利用现代CPU高速缓存预读功能获得最佳性能。


2
2017-08-31 08:41



谢谢 !你认为a之间存在性能差异吗? struct 含6 double 和一个6个双打的静态数组? - Cedric H.
我绝对肯定没有任何区别,表现明智。但可读性方面可能存在一些差异。 - Didier Trosset


你可以走几条路。但在你的情况下,  宣布一个std::vector<std::vector<double> >。你要分配一个 vector (并且你复制它)每6个双打。这太昂贵了。


1
2017-08-31 08:37





如果您认为这是最佳选择,是否可以使用定义为other_array [i,j] //与other_array [6 * i + j]相同的索引运算符来访问此向量没有开销(如每次访问时的函数调用)?

other_array[i,j] 不会工作得太好,因为我,j使用逗号运算符来评估“i”的值,然后丢弃并评估并返回“j”,所以它相当于 other_array[i])。

您将需要使用以下之一:

other_array[i][j]
other_array(i, j)  // if other_array implements operator()(int, int),
                   // but std::vector<> et al don't.
other_array[i].identifier // identifier is a member variable
other_array[i].identifier() // member function getting value
other_array[i].identifier(double) // member function setting value

如果您发现它们有用,您可能会或者可能不会更喜欢将get_和set_或类似物放在最后两个函数上,但是从您的问题我认为您不会:函数优先于API涉及许多开发人员的大型系统的部分,或者当数据项可能不同时,您希望处理数据的算法独立于此。

所以,一个很好的测试:如果你发现自己编写的代码就像 other_array[i][3] 你已经决定“3”是速度的双倍,并且 other_array[i][5] 因为“5”是加速度,然后停止这样做并给它们正确的标识符,这样你就可以说 other_array[i].speed 和 .acceleration。然后其他开发人员可以阅读并理解它,并且你不太可能犯下意外错误。另一方面,如果你迭代这6个元素对每个元素做完全相同的事情,那么你可能确实希望粒子持有双[6],或者提供一个 operator[](int)。两者都没有问题:

struct Particle
{
    double x[6];
    double& speed() { return x[3]; }
    double speed() const { return x[3]; }
    double& acceleration() { return x[5]; }
    ...
};

BTW /原因 vector<vector<double> > 可能成本太高的是,每组6个双打将在堆上分配,并且为了快速分配和释放,许多堆实现使用固定大小的桶,因此您的小请求将被舍入到下一个大小:可能是显着的开销。外部向量还需要记录指向该内存的额外指针。此外,堆分配和释放相对较慢 - 在您遇到这种情况时,您只能在启动和关闭时执行此操作,但没有特别的要点让您的程序无缘无故地变慢。更重要的是,堆上的区域可能只是在内存中,因此您的operator []可能会有缓存错误拉入更多不同的内存页面,从而减慢整个程序的速度。换句话说,向量连续存储元素,但指向向量可能不是连续的。


1
2017-08-31 09:56