哪种方式最适合缓存?

12

我正在努力掌握数据导向设计以及如何在编程时更好地考虑缓存。基本上有两种情况,我还不能确定哪一种更好,也不知道原因 - 是使用对象的向量好,还是使用几个包含对象原子数据的向量比较好?

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

    ....
}

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


1
你的结构体看起来不够大,以至于它并没有真正产生影响,但如果它很大,我会期望你的第一个选项有最少的缺失。 - Sam I am says Reinstate Monica
2
这不取决于访问模式吗?也就是说,如果您经常只访问其中几个元素但读取它们的所有数据,第一种选项看起来更有前途;而如果您通常只使用一个成员变量,则第二种选项更好?(这只是一个猜测。) - us2012
请参见以下网址:https://dev59.com/eWsy5IYBdhLWcg3wsQJj - zch
这里还有一个:数组结构和结构数组的性能差异 - Sage
1
你知道什么会更高效的缓存吗?如果你使用GL_UNSIGNED_SHORT作为索引数据类型。在GPU中,如果你的顶点数少于65537个,你可以通过使用16位索引来提高后T&L缓存和标记的效率。你可能认为对于少于257个顶点的缓冲区,8位索引逻辑上会更进一步提高性能,但大多数硬件不支持本地8位索引。 - Andon M. Coleman
显示剩余2条评论
4个回答

5

针对不同级别的高速缓存,缓存的工作方式可能存在一些差异,具体如下:

  • 如果数据已经在缓存中,则访问速度非常快;
  • 如果数据不在缓存中,则需要付出代价,但是整个缓存行(或页面,如果我们讨论的是RAM vs swap file而不是cache vs RAM)将被加载到缓存中,因此接近缺失地址的访问不会缺失;
  • 如果你很幸运,内存子系统会检测到顺序访问并预取它认为你即将需要的数据。

所以最初的问题是:

  1. 有多少次缓存缺失?--B胜出,因为在A中,每个记录都会获取一些未使用的数据,而在B中,除了迭代末尾的小舍入误差之外,没有获取任何数据。因此,假设记录数量相当大,为了访问所有必要数据,B获取的缓存行较少。如果记录数量不足,则缓存性能可能与代码性能无关,因为使用少量数据的程序将发现其始终处于缓存状态。
  2. 访问是否是顺序的?--两种情况都是,虽然在情况B中可能更难检测,因为有两个交错的序列,而不是只有一个。

因此,我认为就这段代码而言,B可能会更快些。不过:

  • 如果这是数据的唯一访问方式,则可以通过从struct中删除大部分数据成员来加速A。因此,请这样做。实际上,它很可能不是程序中数据的唯一访问方式,而其他访问可能会以两种方式影响性能:它们实际消耗的时间和它们是否将缓存与所需数据填充。
  • 我的期望和实际情况通常不同,如果您有任何测试的能力,依赖猜测没有什么用处。在最好的情况下,顺序访问意味着两种代码都没有缓存缺失。测试性能不需要任何特殊工具(尽管它们可以使其变得更容易),只需要一只带秒表的手表。在紧急情况下,可以使用手机充电器制作摆。
  • 我已经忽略了一些复杂情况。根据硬件配置,如果你在B上运气不好,那么在最低级别的缓存中,你可能会发现访问一个向量的访问正在逐出访问另一个向量的访问,因为相应的内存恰好使用了缓存中相同的位置。这将导致每个记录出现两次缓存缺失。这只会在所谓的“直接映射高速缓存”上发生。"两路高速缓存"或更好的方式可以拯救全局,即使它们在缓存中的首选位置相同,也允许两个向量的块共存。我不认为PC硬件普遍使用直接映射高速缓存,但我不能确定,也不太了解GPU。

现代英特尔CPU(Sandy/Ivy Bridge)具有8路L1和L2缓存和12路L3。我不确定AMD的情况。我也非常确定,大多数具有超过4k L1缓存的ARM处理器都是4路。 - Mart

1

我明白这部分内容可能是基于观点的,也可能是过早优化的情况,但您的第一个选项明显具有最佳美学效果。在我的眼中,一个矢量和六个矢量相比根本不算什么。

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

使用结构方法时,网格本质上是一个自包含的对象,并且正确地不涉及其他网格的关系。在绘制时,您只访问那个网格,在渲染所有网格时,以一种友好的方式逐个渲染。是的,由于矢量元素较大,您将更快地耗尽缓存,但您不会争夺它。

您还可以在以后使用此表示法时获得其他好处。例如,如果要存储与网格相关的其他数据。在更多矢量中添加额外数据将很快使您的代码混乱,并增加出错的风险,而在结构中进行更改非常简单。


2
在GPU架构中,通常会针对大规模并行内存访问进行优化操作;它们针对SoA进行了优化,因为单个操作可以读取许多连续的内存位置,而AoS则需要在元素之间进行跨度。 - DanielKO
可以访问两个不同的向量,这会分割内存访问。我认为这并不一定会导致缓存不友好。简单来说,你的一半缓存可以用于缓存一个向量,同时另一半可以缓存另一个向量(还有第三个半部分缓存堆栈)。由于B中每个向量比A中的向量大小小得多,因此这至少有潜力获得收益。 - Steve Jessop

1
我建议使用perfoprofile进行性能分析,并将结果发布在这里(假设您正在运行linux),包括您迭代的元素数量,总迭代次数和测试硬件信息。
如果我必须猜测(这只是一个猜测),我会认为由于每个结构内数据的局部性以及操作系统/硬件可以为您预取其他元素,因此第一种方法可能更快。但是,这将取决于缓存大小,缓存行大小和其他方面。
定义“更好”的方式也很有趣。您是在寻找处理N个元素的总时间,每个样本的低方差,最小化缓存未命中(这将受到运行在您系统上的其他进程的影响)等方面。
不要忘记,对于STL向量,您还要考虑分配器...例如,它可以随时决定重新分配数组,这将使您的缓存无效。如果可以的话,尝试隔离另一个因素!

很遗憾,我不能运行在Linux上。 - KaiserJohaan
我怀疑在Windows上也会有适合这种事情的好的分析工具...甚至Windows性能计数器也是一个不错的起点。 - Carl Cook

0

取决于您的访问模式。您的第一个版本是AoS(结构数组),第二个版本是SoA(数组结构)

如果在AoS表示中通常会获得任何类型的结构填充,则SoA倾向于使用更少的内存(除非您存储了非常少的元素,以至于数组的开销实际上并不重要)。但是,与AoS相比,它也往往更难编写代码,因为您必须维护/同步并行数组。

AoS在随机访问方面表现出色。举个例子,为了简单起见,假设每个元素都适合于缓存行并且对齐(例如64字节大小和对齐)。在这种情况下,如果您随机访问第n个元素,则可以在单个缓存行中获取该元素的所有相关数据。如果您使用SoA并将这些字段分散到单独的数组中,则必须将内存加载到多个缓存行中,以仅加载该一个元素的数据。而且,由于我们按随机模式访问数据,因此我们几乎没有从空间局部性中获得任何好处,因为我们要访问的下一个元素可能完全位于内存中的其他位置。

然而,SoA往往在顺序访问方面表现出色,主要是因为整个顺序循环中需要加载到CPU缓存的数据通常较少,因为它排除了结构填充和冷字段。所谓冷字段,是指在特定的顺序循环中不需要访问的字段。例如,物理系统可能不关心与粒子外观有关的粒子字段,比如颜色和精灵句柄。这是无关紧要的数据。它只关心粒子位置。SoA允许您避免将那些无关数据加载到缓存行中。它允许您一次性将尽可能多的相关数据加载到缓存行中,从而减少了SoA的强制缓存未命中(以及对于足够大的数据的页面错误)。

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

您还可以混合两个概念。您可以在随机访问模式下频繁访问的热字段中使用AoS,然后将冷字段提取出来并并行存储。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接