【问题标题】:Which is most cache friendly?哪个对缓存最友好?
【发布时间】:2013-10-08 04:56:11
【问题描述】:

我正在努力掌握面向数据的设计以及如何在考虑缓存的情况下进行最佳编程。基本上有两种情况,我无法完全确定哪个更好以及为什么 - 拥有一个对象向量还是多个具有对象原子数据的向量更好?

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

    ....
}

哪一个更节省内存和缓存友好,从而导致更少的缓存未命中和更好的性能,为什么?

【问题讨论】:

  • 你的结构看起来不够大,无法真正发挥作用,但如果它很大,我希望你的第一个选择有最少的失误
  • 我在主机游戏编程中听说您应该尝试将相同类型的数据保持在附近(即第二种方法);像第一个这样的混合内容数据是一个禁忌。但我不确定该建议的相关性。
  • 这不取决于访问模式吗?即,如果您只访问其中几个元素 - 但读取它们的所有数据 - 经常,第一个选项看起来更有希望,而如果您通常只使用一个成员变量,那么第二个看起来更好? (不过这只是猜测。)
  • 建议你用opengl标记这个
  • 你知道什么缓存效率更高吗?如果您使用 GL_UNSIGNED_SHORT 作为索引数据类型。在 GPU 中,如果您的顶点少于 65537 个,则可以通过使用 16 位索引来提高 T&L 后缓存和标记的效率。您可能认为 8 位索引在逻辑上会为顶点数少于 257 的缓冲区进一步提高性能,但大多数硬件本身并不支持 8 位索引。

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


【解决方案1】:

根据您所说的缓存级别有所不同,缓存的工作方式如下:

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

天真的问题是:

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

所以,我有点期望 B 会更快 这段代码。然而:

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

【讨论】:

  • 现代 Intel CPU(Sandy/Ivy Bridge)具有 8 路 L1 和 L2 缓存以及 12 路 L3。不确定AMD。我也很确定大多数具有 4k 以上 L1 缓存的 ARM 处理器都是 4 路的。
【解决方案2】:

我知道这部分是基于意见的,也可能是过早优化的情况,但您的第一个选项绝对具有最佳美学效果。这是一个向量与六个向量 - 在我看来没有竞争。

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

使用结构方法,网格本质上是一个独立的对象,并且正确地暗示与其他网格没有关系。绘制时,您只能访问 那个 网格,而在渲染 所有 网格时,您以缓存友好的方式一次执行一个。是的,你会更快地吃掉缓存,因为你的向量元素更大,但你不会竞争它。

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

【讨论】:

  • 在 GPU 架构上,为大规模并行内存访问优化操作是很常见的;它们针对 SoA 进行了优化,因为单个操作可以读取许多连续的内存位置,而 AoS 则需要元素之间的跨步。
  • “访问两个不同的向量,这会拆分内存访问”。我不认为这本质上对缓存不友好。为了过度简化,一半的缓存可用于缓存一个向量,而另一半同时可以缓存另一个向量(第三半缓存堆栈)。由于 B 中的每个向量都远小于 A 中向量的一半大小,因此这至少有可能获胜。
【解决方案3】:

我建议使用perfoprofile 进行分析并将结果发回此处(假设您正在运行Linux),包括您迭代的元素数量、总迭代次数以及您测试的硬件.

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

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

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

【讨论】:

  • 不幸的是没有运行 linux
  • 我怀疑在 Windows 上也会有很好的分析器来处理这类事情……即使是 windows performance counters 也是一个好的开始。
【解决方案4】:

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

SoA 往往会使用更少的内存(除非您存储 这么少 元素,以至于数组的开销实际上是不小的),如果有任何类型的结构填充,您通常会在AoS 表示。它也往往是一个更大的 PITA 编码,因为您必须维护/同步并行数组。

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

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

这也只涉及内存访问模式。借助 SoA 代表,您还可以编写更高效、更简单的 SIMD 指令。但同样它主要适用于顺序访问

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-12
    • 2013-05-17
    • 2023-03-17
    相关资源
    最近更新 更多