【发布时间】:2015-07-15 22:46:41
【问题描述】:
苦乐参半的 SoA
我最近看到了使用带有 SoA(数组结构)表示的手写 SIMD 内在函数的乐趣。
与我以前的 AoS(结构数组)代码相比,速度提升(至少对于简单的顺序类型流操作)几乎是惊人的,速度提升了一倍到三倍。作为奖励,除了减少内存使用之外,它还简化了逻辑以排除那些棘手的水平操作和混洗组件。
然而,后来我意识到他们在代码中使用的 PITA 是多么的苦乐参半,尤其是界面设计。
中层界面设计
我经常处理中级界面的设计。它们比 std::vector 级别更高,但比视频游戏中的 Monster 级别低。对于我来说,这些总是一些最难设计和保持稳定的接口,因为它们不够低级,无法像标准 C++ 容器那样提供简单的读/写接口。然而,它们还不够高级(在接口的入口点中缺乏足够的逻辑),无法完全隐藏和抽象出底层表示,只提供高级操作。
我认为中级设计的一个例子是可编程粒子系统 API,它希望在某些场景下尽可能高效和可扩展,同时方便临时场景(例如:脚本编写者)。这样的设计必须提供粒子访问,除非它为与可想象的粒子相关的所有可能算法提供一种方法,否则它必须在某处、某处公开一些原始 SoA 细节,以让客户从中受益。
设计也不一定要求始终编写 SoA 类型代码。更多的日常使用仍然不要求最大的效率,而是方便、简单、生产力。它仅适用于底层 SoA 表示派上用场的那些罕见的、对性能至关重要的场景。
那么 API/lib 设计人员和大型系统人员如何平衡这些类型的需求?
平衡多种访问模式
由于 SoA 消除了任何每个元素的结构,当用户使用界面中更方便的随机访问部分访问 nth 元素时,动态实例化结构/类可能是一个不错的主意吗?也许是一个包含指向多个 SoA 数组的第 n 个条目的指针/引用以进行可变访问的结构?
此外,如果更常见的使用模式是更随机访问的标量逻辑而不是顺序访问的 SIMD 矢量逻辑,但 SIMD 部分被触发到足以仍然使仅使用一个数据结构更好,这可能吗?哪种混合 SoA 表示能更好地平衡所有需求?
struct AoSoA
{
ALIGN16 float x[4];
ALIGN16 float y[4];
ALIGN16 float z[4];
};
ALIGN16 AoSoA elements[n/4];
我不太了解缓存行的性质,因此不知道这种表示是否值得。我注意到它对于我们可以将全部资源用于一个庞大的算法的顺序 SIMD 案例并没有太大帮助,但它似乎对于需要跨组件或随机访问大量水平逻辑的案例很有帮助系统可能同时执行许多其他操作的标量逻辑案例。
无论如何,我通常都在寻找有关如何有效地设计中间级数据结构接口的见解,并将 SoA 后端表示作为实现细节,而不会将复杂性转移给客户端,除非他们真的想要它。
我真的很想避免强迫客户总是在每个使用接口的地方编写 SoA 类型的代码,除非他们真的需要这种效率,而且我很好奇如何平衡那些更日常的、随机访问的标量使用场景与利用 SoA 表示的罕见但并不少见的场景。
【问题讨论】:
-
也许是标准库使用的策略,其中算法接口总是采用迭代器而不是对容器的引用?迭代器隐藏底层结构并使其无关紧要。
-
@MarkRansom 我有点递归到迭代器需要返回类似于完整元素(例如完整粒子)的位置,但也许我可以通过聚合所有这些 SoA 数组的第 n 个元素。
-
@MarkRansom 我想做的一件事,它更多地处理后端实现而不是接口,是像
Container<T1, T2, T3, ...>一样进行概括,它为每个Tn存储那些并行对齐的 SoA 数组,将value_type定义为具有N成员的结构(元组),我们可以动态创建包含由其迭代器和运算符[] 返回的第n 个元素的组件。我不确定这是否矫枉过正,但它会将处理 SoA 的一些尴尬隐藏到一个通用容器中进行测试和维护。 -
不是直接回答您的问题,但 Insomniac 今年在 GDC 上有一个关于他们引擎中的 SIMD 的非常好的演讲,您可能会觉得有趣:gdcvault.com/play/1022249/SIMD-at-Insomniac-Games-How
-
@mattnewport Neat,我去看看——谢谢!
标签: c++ optimization architecture simd