哪个缓存最友好?

时间:2013-10-01 21:25:07

标签: c++ opengl caching memory-management data-oriented-design

我正在努力抓住面向数据的设计以及如何最好地编写缓存。基本上有两种情况我无法确定哪种更好以及为什么 - 拥有对象矢量或带有对象原子数据的几个向量是否更好?

A)对象矢量示例

struct A
{
    GLsizei mIndices;
    GLuint mVBO;
    GLuint mIndexBuffer;
    GLuint mVAO;

    size_t vertexDataSize;
    size_t normalDataSize;
};

std::vector<A> gMeshes;

for_each(gMeshes as mesh)
{
    glBindVertexArray(mesh.mVAO);
    glDrawElements(GL_TRIANGLES, mesh.mIndices, GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);

    ....
}

B)具有原子数据的载体

std::vector<GLsizei> gIndices;
std::vector<GLuint> gVBOs;
std::vector<GLuint> gIndexBuffers;
std::vector<GLuint> gVAOs;
std::vector<size_t> gVertexDataSizes;
std::vector<size_t> gNormalDataSizes;

size_t numMeshes = ...;

for (index = 0; index++; index < numMeshes)
{
    glBindVertexArray(gVAOs[index]);
    glDrawElements(GL_TRIANGLES, gIndices[index], GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);

    ....
}

哪一个更有内存效率和缓存友好性,从而减少缓存未命中和性能,以及为什么?

4 个答案:

答案 0 :(得分:5)

根据您所讨论的缓存级别的某些变化,缓存的工作方式如下:

  • 如果数据已经在缓存中,则访问速度很快
  • 如果数据不在缓存中,则会产生成本,但整个缓存行(或页面,如果我们正在谈论RAM与交换文件而不是缓存与RAM)将被带入缓存,因此访问接近于错过的地址不会错过。
  • 如果你很幸运,那么内存子系统将检测它认为你将需要的顺序访问和预取数据。

要问的问题是天真的:

  1. 发生多少缓存未命中? - B获胜,因为在A中,每个记录获取一些未使用的数据,而在B中,您只能在迭代结束时获取一个小的舍入误差。因此,为了访问所有必要的数据,B采用较少的缓存行,假设有大量的记录。如果记录数量无关紧要,那么缓存性能可能与代码的性能几乎没有任何关系,因为使用足够少量数据的程序会发现它始终都在缓存中。
  2. 是访问顺序? - 在两种情况下都是,但在B情况下可能更难检测,因为有两个交错序列而不是一个。
  3. 所以,我希望B对于此代码更快。但是:

    • 如果这是对数据的唯一访问权限,那么您可以通过从struct中删除大多数数据成员来加速A.那样做吧。据推测,事实上它不是对程序中数据的唯一访问,而其他访问可能会以两种方式影响性能:它们实际占用的时间,以及它们是否使用您需要的数据填充缓存。
    • 我的期望和实际发生的事情往往是不同的事情,如果你有任何测试能力,就没有必要依靠猜测。在最好的情况下,顺序访问意味着在任一代码中都没有缓存未命中。测试性能要求没有特殊工具(虽然它们可以使它更容易),只是一个带秒针的时钟。在紧要关头,用手机充电器摆一个钟摆。
    • 我忽略了一些并发症。根据硬件的不同,如果您不熟悉B,那么在最低缓存级别,您会发现对一个向量的访问正在逐出对另一个向量的访问,因为相应的内存恰好在缓存中使用相同的位置。这将导致每个记录两个缓存未命中。这只会发生在所谓的“直接映射缓存”上。通过允许两个向量的块共存,即使它们在高速缓存中的第一个首选项位置相同,“双向高速缓存”或更好也可以节省时间。我不认为PC硬件通常使用直接映射缓存,但我不确定,我对GPU知之甚少。

答案 1 :(得分:1)

我知道这部分是基于意见的,而且它可能是一个过早优化的情况,但你的第一个选择肯定是最好的美学。这是一个矢量与六个 - 我眼中没有比赛。

对于缓存性能,它应该更好。这是因为替代方案需要访问两个不同的向量,每次渲染网格时都会分割内存访问。

使用结构方法,网格本质上是一个自包含的对象,并且正确地暗示与其他网格无关。绘图时,只访问那个网格物体,当渲染所有网格物体时,您可以以缓存友好的方式一次执行一个。是的,你会更快地吃缓存,因为你的矢量元素更大,但你不会参与竞争。

您可能会在以后使用此表示找到其他好处。 ie 如果要存储有关网格的其他数据。在更多向量中添加额外数据会很快混乱您的代码并增加制造愚蠢错误的风险,而对结构进行更改则是微不足道的。

答案 2 :(得分:1)

我建议使用perfoprofile进行性能分析,并将结果发布到此处(假设您正在运行linux),包括迭代的元素数量,总计迭代次数以及你测试的硬件。

如果我不得不猜测(这只是一个猜测),我怀疑第一种方法可能会更快,因为每个结构中的数据的位置,并希望操作系统/硬件可以为您预取其他元素。但同样,这将取决于缓存大小,缓存行大小和其他方面。

定义“更好”也很有趣。您是否正在寻找处理N个元素的总时间,每个样本的低差异,最小的缓存未命中(这将受到系统上运行的其他进程的影响)等。

不要忘记使用STL向量,你也受分配器的支配......例如它可以随时决定重新分配数组,这将使您的缓存无效。如果可以的话,尝试隔离的另一个因素是!

答案 3 :(得分:0)

取决于您的访问模式。您的第一个版本是AoS (array of structures),第二个版本是SoA (structure of arrays)

SoA倾向于使用更少的内存(除非你存储如此少的元素,使得数组的开销实际上是非常重要的)如果有任何类型的结构填充,你可以使用#39 ; d通常进入AoS表示。它也往往是一个更大的PITA代码,因为你必须维护/同步并行数组。

AoS往往优于随机访问。作为示例,为简单起见,假设每个元素都适合高速缓存行并且被正确对齐(例如,64字节大小和对齐)。在这种情况下,如果您随机访问nth元素,则会在单个缓存行中获取该元素的所有相关数据。如果您使用SoA并将这些字段分散在不同的数组中,则必须将内存加载到多个缓存行中才能加载该元素的数据。而且因为我们以随机模式访问数据,所以我们根本不会从空间局部性中受益,因为我们将要访问的下一个元素可能完全存在于内存中。< / p>

然而,SoA往往优于顺序访问,主要是因为它通常在整个顺序循环中首先加载到CPU缓存中的数据较少,因为它排除了结构填充和冷场。冷场,我的意思是你不需要在特定的顺序循环中访问的字段。例如,物理系统可能不关心粒子如何看待用户的粒子场,如颜色和精灵手柄。这是无关紧要的数据。它只关心粒子位置。 SoA允许您避免将不相关的数据加载到缓存行中。它允许您同时将相关数据加载到缓存行中,因此您最终可以使用SoA减少强制缓存未命中(以及足够大的数据的页面错误)。

这也仅涵盖了内存访问模式。使用SoA代表,您也倾向于编写更有效和更简单的SIMD指令。但同样,它主要适用于顺序访问

您也可以混合使用这两个概念。您可以将AoS用于经常以随机访问模式一起访问的热字段,然后提升冷字段并将它们并行存储。