哪个缓存最友好?

问题描述 投票:10回答:4

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

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);

    ....
}

哪一个更有内存效率和缓存友好性,导致更少的缓存未命中和更好的性能,为什么?

c++ opengl caching memory-management data-oriented-design
4个回答
5
投票

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

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

天真地问的问题是:

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

所以,我希望B对这段代码更快。然而:

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

1
投票

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

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

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

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


1
投票

我建议使用perfoprofile进行分析,然后将结果发布到此处(假设您运行的是linux),包括迭代的元素数,总共迭代次数以及您测试的硬件。

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

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

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


0
投票

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

如果存在通常在AoS表示中获得的任何类型的结构填充,则SoA倾向于使用更少的内存(除非您存储的元素开销实际上非常平凡的元素很少)。由于必须维护/同步并行数组,因此它往往是一个更大的PITA代码。

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

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

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

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

© www.soinside.com 2019 - 2024. All rights reserved.