添加快捷方式
分享
储存顶点数据:交错还是不交错?(翻译)
输入“/”快速插入内容
储存顶点数据:交错还是不交错?(翻译)
原文:
《Storing vertex data: To interleave or not to interleave?》
最近,我一直在重构我的主框架中的几何存储。此外,我还研究了顶点属性存储,这是我们今天要深入研究的内容。
说到顶点数据的存储,基本上有两种不同的思想流派。一种是将顶点属性交错在一起,也就是说,将位置、法线、UV坐标等属性存储在一起,形成一个“臃肿”的顶点。我将称之为
交错存储
,因为它交错了内存中的所有顶点属性。另一派则认为所有的属性都应该保持独立,所以一个顶点由多个流组成。每个流只使用紧密包装存储一个属性。
为什么要关心这个?
让我们看看顶点属性存储在不同地方的影响:
•
在磁盘上
,因为压缩和读性能可能会受到影响。
•
在内存中
,因为一些算法偏爱一种顺序或另一种。
•
在渲染时
,因为它影响所需的带宽和gpu的性能。
渲染
我们首先看最后一个原因,即GPU渲染,因为它是最容易解释的。在GPU上,所有API都允许从多个流或单个流获取顶点属性。这使得实验非常简单,也突出了一些关键的区别。
📌
在D3D11中,
IASetVertexBuffers
函数的第一个参数是
UINT StartSlot
,这个函数多次调用且每次传入的
StartSlot
不同,来实现从多个流获取顶点属性。
配套的顶点结构声明结构体
D3D11_INPUT_ELEMENT_DESC
的
UINT InputSlot
要填上和上文的
StartSlot
对应的值。
首先受到影响的是
访问灵活性
。我有一个几何查看器,它可能有,也可能没有一个网格的所有属性。对于交错数据,很难关闭一个属性,因为顶点布局(layout)需要调整。使用去交错数据,就像绑定一个空缓冲区或使用一个只是跳过通道的着色器排列一样简单。去交错数据得1分。
下一个用例是仅使用位置属性渲染,这在阴影图(Shadow Mapping)中非常常见。同样,由于
缓存效率
,去交错数据在这里胜出。这很容易看出——如果你只需要位置,而且你将位置与其他属性分离,则可以获得最佳的缓存和带宽利用率。对于交错数据,每条缓存行(Cache Line)都会从内存获取一些你将立即丢弃的其他属性。去交错数据再得1分。
从交错存储各种属性的顶点读取位置。单个缓存行(Cache Line)只够存储4个顶点,并且超过一般的缓存行被丢弃。
最后一点对于GPU来说非常重要。在GPU计算单元上,你有非常宽的向量单元,它们想要在给定的周期内获取所有相同的属性,例如,位置。如果数据是去交错的,它们可以将数据取到寄存器中并立即清除缓存行。你可以从上图中看到这一点。在第一次迭代中,首先读取红色的x坐标,然后是y坐标,最后是z坐标。因此,三次读取就消耗了整个缓存行,并且它本来能被立即删除。对于交错数据,数据必须保持在缓存中,直到所有数据都被读取,从而污染了已经很小的缓存——因此,由于缓存利用率更好,去交错数据将渲染得稍微快一些。
从去交错顶点缓冲区读取位置属性。位置属性被紧密地存储,十个顶点的位置可以被一个缓存行处理。
是否真的有一个很好的理由使用交错数据来渲染?事实上,我想不出一个,而且事实证明,几年前我就把我的几何查看器改成了去交错数据,而且再也没有回头看:)
在离线渲染的世界中,由于射线跟踪器主要关心位置,属性也单独指定。对于这个用例,缓存效率是最重要的,因此你也希望它们是独立的,甚至在CPU上也是如此。
计算
下面是更有趣的部分。在最近的重构过程中,我修改了
网格视图抽象
,以便在获取单个属性时利用去交错数据。因此,我现有的所有算法都需要重构,以处理交错数据和去交错数据,这让我对每种算法的优缺点有了很好的了解。
事实证明,在我的工具箱中只有一种算法非常需要交错数据来提高性能,它会在遇到去交错网格时重新交错。这个算法是重索引器,它通过存储一个顶点散列码和一个指针来搜索唯一的顶点,这样它就可以进行精确的比较。
除了这个算法之外,所有其他算法都只用到一个属性,主要是位置,现在对去交错数据的缓存效率会稍微高一些。我简单地测量了一下性能,但结果是,对于具有位置、法线和可能有一两个以上属性的“苗条”顶点,cpu上的缓存效率差异是非常小的——我希望在重多线程和带宽受限的情况下获得更多的收益。好消息是没有任何东西变慢了。
我认为这是平局,因为重索引器的缘故。由于我现在向所有算法开放了一个指针和stride,所以在表示之间进行交换基本上是微不足道的。对于重索引器,我认为一定有比指针和哈希更好的方法来表示顶点,这也将解决这个问题(也许一个不碰撞的更强哈希将能满足…)
存储
有趣的部分来了。我的几何存储是LZ4压缩的,通过压缩,你可能期望交错数据比非交错数据更容易费时间。毕竟,所有的位置会有相似的指数,所有的法线会有相同的指数等等,如果它们是连续存储的,压缩器应该在数据中发现更多的相关性。
事实证明,使用默认的LZ4压缩,这并不完全正确,交错数据实际上压缩得更好。为了进行测试,我使用了
XYZRGB斯坦福龙
,并将其转换为我的二进制格式,将位置存储为3个float,法线也存储为3个float。