通过指针与数组中的索引链接结构图

时间:2016-04-21 09:24:04

标签: c++ performance graph-traversal 3d-engine

这是关于良好做法

的问题

考虑典型情况,例如:在3D引擎,物理引擎,Finite element methodclassical molecular dynamics求解器中:你有各种类型的对象(例如顶点,边,面,有界实体体积)彼此交叉链接(例如顶点知道)哪个边连接到它,反之亦然)。为了使用这种引擎的性能和便利性,能够快速浏览这种连接的网络是至关重要的。

问题是:通过数组中的索引或指针指向链接对象是否更好? ...尤其是性能方面

typedef index_t uint16_t;

class Vertex{
    Vec3 pos;
    #ifdef BY_POINTER
    Edge*   edges[nMaxEdgesPerVertex];
    Face*   faces[nMaxFacesPerVertex];
    #else
    index_t edges[nMaxEdgesPerVertex];
    index_t faces[nMaxFacesPerVertex];
    #endif
}

class Edge{
    Vec3   direction;
    double length;
    #ifdef BY_POINTER
    Vertex* verts[2];
    Faces*  faces[nMaxFacesPerEdge];  
    #else
    index_t verts[2];
    index_t faces[nMaxFacesPerEdge];
    #endif
}

class Face{
    Vec3   normal;
    double isoVal; // Plane equation: normal.dot(test_point)==isoVal
    #ifdef BY_POINTER
    Vertex* verts[nMaxVertsPerFace];
    Edge*   edges[nMaxEdgesPerFace];
    #else
    index_t verts[nMaxVertsPerFace];
    index_t edges[nMaxEdgesPerFace];
    #endif
}

#ifndef BY_POINTER
// we can use other datastructure here, such as std:vector or even some HashMap 
int nVerts,nEdges,nFaces;
Vertex verts[nMaxVerts];
Edge   edges[nMaxEdges];
Vertex faces[nMaxFaces];
#endif 

索引的优点:

  • 当我们使用uint8_tuint16_t作为索引而不是32位或64位指针时,使用索引可以提高内存效率
  • 索引可以携带一些位编码的附加信息(例如,关于边缘的方向);
  • 数组中对象的排序可以携带一些关于结构的信息(例如,立方体的顶点可以被命名为{0b000,0b001,0b010,0b011,0b100,0b101,0b110,0b111})。此信息在指针
  • 中不可见

指针的优点:

  • 我们不需要关心存储对象的数组(或其他数据结构)。可以通过new Vertex()简单地在堆上动态分配对象。
  • May be faster (?)因为它不需要添加数组的基址(?)。但就内存延迟(?)
  • 而言,这可能是微不足道的

2 个答案:

答案 0 :(得分:5)

为了表现,重要的是你能够快速阅读" next"任何遍历顺序中的元素通常都是在热路径中完成的。

例如,如果您有一系列表示某个路径的边,您可能希望它们连续存储在内存中(不是每个都使用new),按照它们的连接顺序。

对于这种情况(形成路径的边缘),很明显你不需要指针,而且你也不需要索引。连接由存储位置隐含,因此您只需要指向第一个也可能是最后一个边的指针(即您可以将整个路径存储在std::vector<Edge>中)。

第二个例子说明了我们可以利用的领域知识:想象一下,我们有一个游戏支持多达8个玩家,并希望存储&#34;谁已经访问了路径中的每个边缘。&#34;同样,我们不需要指针或索引来引用8个玩家。相反,我们可以简单地在每个uint8_t中存储Edge,并将这些位用作每个玩家的标记。是的,这是低级别的敲击,但是一旦我们有Edge*,它就会为我们提供紧凑的存储和高效的查找。但是如果我们需要从另一个方向进行查找,从玩家到Edge s,最有效的方法是存储,例如每个玩家内部的uint32_t向量,并将其编入索引Edge数组。

但是如果可以在路径中间添加和删除边缘怎么办?那么我们可能想要一个链表。在这种情况下,我们应该使用侵入式链接列表,并在池中分配Edge。完成此操作后,我们可以在每个播放器对象中存储指向Edge的指针,它们永远不会更改或需要更新。我们使用一个侵入式链表,并理解Edge只是单个路径的一部分,因此链表指针的外部存储将是浪费的(std::list需要存储指向每个对象的指针;侵入性列表不会。)

因此,必须单独考虑每个案例,并尽可能多地了解域名。指针和索引都不应该是第一种方法。

答案 1 :(得分:5)

  当我们使用uint8_t或时,使用索引可以提高内存效率   uint16_t用于索引而不是32位或64位指针

真。使用小的表示可以减少结构的总大小,减少遍历时的缓存未命中。

  

索引可以携带一些额外的信息(例如关于方向   边缘编码的某些位;

真。

  

我们不需要关心数组(或其他数据结构)   存储对象。可以简单地动态分配对象   通过新的Vertex()堆。

这正是你不想做的,谈到表演。 您希望确保Vertex全部打包,以避免不必要的缓存丢失。 在这种情况下,数组可以避免错误的诱惑。 您还希望至少尽可能多地按顺序访问它们,以最大限度地减少缓存未命中。

数据结构的打包量,小数量和按顺序访问的数量实际上是驱动性能的。

  

可能更快(?)因为它不需要添加基地址   数组(?)。但就记忆而言,这可能是微不足道的   延迟(?)

可能微不足道。可能取决于特定的硬件和/或编译器。

关于索引的另一个缺失优势:重新分配时更容易管理。 考虑一个可以增长的结构,如下所示:

struct VertexList
{
  std::vector<Vertex> vertices;

  Vertex *start; // you can still access using vector if you prefer; start = &vertices[0];
}

如果使用指针引用给定顶点,并且发生重新分配,则最终会出现无效指针。