【问题标题】:C++ Designing for SIMD: Making an SoA less of a PiTA [closed]为 SIMD 设计的 C++:使 SoA 不再是 PiTA [关闭]
【发布时间】: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


【解决方案1】:

我实际上没有足够的软件工程知识来制定您想要做的一般策略,但特别是对于 AoS 与 SoA 问题,我发现 Robert Strzodka 的这篇论文引人入胜:http://asc.ziti.uni-heidelberg.de/sites/default/files/research/papers/public/St11ASX_CUDA.pdf

这种抽象的目标是提供一种在 AoS 和 SoA 以及更复杂的嵌套之间切换的简单方法。作者使用它来展示性能如何随着不同的访问模式而变化,而无需触及算法部分,也无需重新编码所有访问。

虽然它更侧重于 GPU 方面,但提供的代码也适用于 CPU。

【讨论】:

    【解决方案2】:

    到目前为止,我已经在内部找到了这种“混合 SoA”或“AoSoA”代表的合适人选。

    struct HybridSoA
    {
         ALIGN float x[4];
         ALIGN float y[4];
         ALIGN float z[4];
    };
    

    它通过为随机访问路径保留合理空间局部性的设计来平衡使用 SIMD 的顺序快速路径与随机访问和不真正关心 SIMD 的较慢路径。

    对于接口,我还没有太花哨,只是为那些快速顺序路径和代理返回指向这些结构的指针,从而允许对operator[] 等进行标量式访问。

    接口类型泄漏了 SIMD 路径的一些内部结构,但这似乎是不可避免的,因为设计无法预测所有高级需求而不会变得越来越单一,并且它以某种方式抽象,并且对 ABI 有严格的关注使得使用更丰富的接口变得困难(实际的接口是用 C 编码的,顶部有一个 C++ 包装器)。

    如果我提供一种接受函数指针的foreach 方法(或最终转换为std::function 之类的方法,尽管由于ABI 原因我不能直接使用它),也许它可能会更好被回调而不是直接暴露内部句柄。它可以批量输入 SIMD 所需的 SoA 数据以减轻调用开销,这将缓解我遇到的时间耦合问题,即对结构的写访问需要显式的 commit 调用来记录对应用程序历史的更改.

    如果迭代器可以兼作以代理样式形式访问数据的方式(更少的原始暴露),它们可能会很好。尽管我已经不再喜欢除通用容器之外的所有迭代器,尤其是在相关算法不属于通用类别的情况下。在过去,我发现为所有事物维护迭代器是一种负担(这种负担超过了使用基于范围的 for over operator[] 的好处,例如),并且开始支持 C 和 C++ 之间的一种混蛋美学(仅适用于这些中级数据结构,它们比标准容器更复杂并存储不同类型的数据,但还不足以对通用容器之外的公共接口施加许多约束)。

    仅对于这些特定类型的数据结构,我发现偏爱普通的旧 C 样式数组的界面美学最有成效,尽管这肯定是有偏见的,而且肯定只是我自己的倾向的结果。对于像网格这样的东西,我只是发现自己越来越被 C 风格的美学所吸引,这只是因为我过去在这些情况下一直犯错太多层的代码,以至于我对我的自己的创作。

    感谢迄今为止所有的答案和cmets!

    【讨论】:

      猜你喜欢
      • 2019-09-19
      • 2011-06-24
      • 2014-04-30
      • 2021-01-04
      • 1970-01-01
      • 2011-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多