我正在努力抓住面向数据的设计,以及如何最好地编写缓存。基本上有两种情况我无法确定哪种更好以及为什么 - 有一个对象向量或带有对象原子数据的几个向量是否更好?
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);
....
}
哪一个更有内存效率和缓存友好性,导致更少的缓存未命中和更好的性能,为什么?
根据您所讨论的缓存级别的一些变化,缓存的工作方式如下:
天真地问的问题是:
所以,我希望B对这段代码更快。然而:
struct
中删除大多数数据成员来加速A.那样做吧。据推测,实际上它不是对程序中数据的唯一访问,而其他访问可能会以两种方式影响性能:它们实际占用的时间,以及它们是否使用您需要的数据填充缓存。我知道这部分是基于意见的,也可能是过早优化的情况,但你的第一个选择肯定是最好的美学。这是一个矢量与六个 - 我眼中没有比赛。
对于缓存性能,它应该更好。这是因为替代方案需要访问两个不同的向量,每次渲染网格时都会分割内存访问。
使用结构方法,网格本质上是一个自包含的对象,并且正确地暗示与其他网格无关。绘图时,您只能访问该网格,并且在渲染所有网格时,您可以以缓存友好的方式一次执行一个网格。是的,你会更快地吃缓存,因为你的矢量元素更大,但你不会参与竞争。
您可能还会在以后使用此表示找到其他好处。即,如果要存储有关网格的其他数据。在更多向量中添加额外数据会使代码快速混乱并增加制造愚蠢错误的风险,而对结构进行更改则微不足道。
我建议使用perf或oprofile进行分析,然后将结果发布到此处(假设您运行的是linux),包括迭代的元素数,总共迭代次数以及您测试的硬件。
如果我不得不猜测(这只是一个猜测),我怀疑第一种方法可能会更快,因为每个结构中的数据的位置,并希望操作系统/硬件可以为您预取其他元素。但同样,这将取决于缓存大小,缓存行大小和其他方面。
定义“更好”也很有趣。您是否正在寻找处理N个元素的总时间,每个样本的低差异,最小的缓存未命中(这将受到系统上运行的其他进程的影响)等。
不要忘记使用STL向量,你也受分配器的支配......例如它可以随时决定重新分配数组,这将使您的缓存无效。如果可以,尝试隔离的另一个因素!
取决于您的访问模式。你的第一个版本是AoS (array of structures),第二个版本是SoA (structure of arrays)。
如果存在通常在AoS表示中获得的任何类型的结构填充,则SoA倾向于使用更少的内存(除非您存储的元素开销实际上非常平凡的元素很少)。由于必须维护/同步并行数组,因此它往往是一个更大的PITA代码。
AoS往往优于随机访问。作为示例,为简单起见,假设每个元素适合高速缓存行并且被正确对齐(例如,64字节大小和对齐)。在这种情况下,如果您随机访问nth
元素,则可以在单个缓存行中获取该元素的所有相关数据。如果您使用SoA并将这些字段分散到不同的数组中,则必须将内存加载到多个缓存行中才能加载该元素的数据。而且因为我们以随机模式访问数据,所以我们根本不会从空间局部性中受益,因为我们要访问的下一个元素可能完全在内存中。
但是,SoA往往优于顺序访问,主要是因为在整个顺序循环中首先加载到CPU缓存中的数据通常较少,因为它排除了结构填充和冷字段。在冷场中,我指的是您不需要在特定的顺序循环中访问的字段。例如,物理系统可能不关心粒子如何看待用户的粒子场,如颜色和精灵手柄。这是无关紧要的数据。它只关心粒子位置。 SoA允许您避免将不相关的数据加载到缓存行中。它允许您同时将相关数据加载到缓存行中,因此您最终可以使用SoA减少强制缓存未命中(以及足够大的数据的页面错误)。
这也仅涵盖了内存访问模式。使用SoA代表,您也倾向于编写更有效和更简单的SIMD指令。但同样,它主要适用于顺序访问。
您也可以混合使用这两个概念。您可以将AoS用于经常以随机访问模式一起访问的热场,然后提升冷场并将它们并行存储。