C ++为SIMD设计:使SoA不再是PiTA

时间:2015-05-06 01:18:43

标签: c++ optimization architecture simd

Bittersweet SoAs

我最近看到了使用手工编写的SIMD内在函数与SoA(数组结构)表示的乐趣。

与我之前的AoS(结构阵列)代码相比,速度的改进,至少对于简单的顺序型流媒体操作而言,在加倍增加三倍速度时几乎没有什么惊人之处。作为奖励,它简化了逻辑,除了减少内存使用外,还排除了那些棘手的水平操作和混乱组件。

然而,之后有这种苦乐参半的感觉,我意识到他们在代码中使用的是什么PITA,尤其是界面设计。

中级界面设计

我经常处理设计中级接口。它们比std::vector更高级,但比视频游戏中的Monster级更低。这些对我来说总是一些最笨拙的接口设计和保持稳定,因为它们不足以提供与标准C ++容器一样的简单读/写接口。然而,它们不够高级(在界面的入口点缺乏足够的逻辑)来完全隐藏和抽象出底层表示,只提供高级操作。

我认为中级设计的一个例子是可编程粒子系统API,它希望在某些场景下尽可能高效和可扩展,同时便于休闲场景(例如脚本编写者)。这样的设计必须提供粒子访问,除非它将为每种可能的算法提供与可想象的粒子相关的方法,否则它必须在某处,某处,在某些地方暴露一些原始SoA细节,以便让客户从中受益。 / p>

也不一定要求设计要求始终写入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表示的罕见但不太常见的场景。

2 个答案:

答案 0 :(得分:1)

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

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

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

答案 1 :(得分:1)

到目前为止,我已经找到了适合这种“混合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调用来记录更改的时间耦合问题。申请历史。

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

对于这些特定类型的数据结构,我发现它最有效地支持普通旧C风格数组的界面美学,尽管这肯定是有偏见的,当然只是我自己的倾向的结果。对于像网格这样的东西,我只是不断发现自己越来越被C风格的美学所吸引,只是因为我过去常常错误地将这些案例的代码层放在一边,让我感到很困惑。自己的创作。

到目前为止,感谢所有的答案和评论!