用于矢量化的随机读取上的结构阵列 (AoS) 与阵列结构 (SoA)
Posted
技术标签:
【中文标题】用于矢量化的随机读取上的结构阵列 (AoS) 与阵列结构 (SoA)【英文标题】:Array of Structures (AoS) vs Structure of Arrays (SoA) on random reads for vectorization 【发布时间】:2015-02-23 14:51:41 【问题描述】:我的问题是关于书中的以下短语:
不幸的是,SoA 形式并非在所有情况下都是理想的。为了 随机或不连贯的情况下,聚集用于访问 数据和 SoA 表单可能会导致读取额外的不需要的数据 进入缓存,从而降低性能。在这种情况下,使用 AoS 相反,表单将导致更小的工作集和改进 表现。但是,一般来说,如果计算是 矢量化,SoA 形式是首选。
我的猜测关于为什么 AoS 可能导致更好的性能是当相同结构中的不同或更好的所有字段参与单个矢量化运行时。
示例(只是一个概念,没有具体的代码或工作代码):
/*Note that the types of data I maintain the same intentionally,
to simplify discussion*/
struct Data
float mean;
float distribution[10]
并定义从某个数据源随机获得的数组
Data aos[5];
现在,如果在矢量化循环期间我执行以下操作:
float* dataPtr = &(aos[0].mean);
#pragma simd
for(int i=0; i< 60; i++)
const float mean = (*dataPtr);
/*do something with mean */
dataPtr++;
/*do something with distribution */
这将带来更好的性能,因为在 SoA 的情况下,我将在缓存行上推送更多我在此计算期间可能实际需要的信息。一些 CPU 预缓存?在 AoS 的情况下,这会带来更好的性能。
我的假设是正确的,还是有别的原因?
【问题讨论】:
可怕的三个字母缩写词:SoA = 数组结构,AoS = 结构数组。 @HansPassant:他们就是这么叫的,写下完整的名字会让标题太长而且不那么难看。 @Tigran:定义一次术语,为了那些不知道你的书术语的人的利益,不需要很长时间。 @MikeSeymour:这根本不是我的书,而是引用这些结构的一般方式。无论如何,更改了问题的标题。 可能相关:***.com/questions/1641580/… 【参考方案1】:您可以通过两种方式并行化您的程序:水平和垂直。我认为您正在混合使用这两种方法。
水平并行化将 SIMD 单元中的每个通道视为处理不同数据的单独“线程”。垂直并行化需要整个 SIMD 单元处理同一个数据对象,试图从其内部的多维性中受益。
举一个具体的例子:假设您有 2 个数组 X
和 Y
要添加的 3D 向量。
水平方法:SIMD 单元的每个通道都可以:
for(idx = 0; idx<size; idx+=SIMD_size)
... = X[idx+laneid].x + Y[idx+laneid].x;
... = X[idx+laneid].y + Y[idx+laneid].y;
... = X[idx+laneid].z + Y[idx+laneid].z;
垂直方法:SIMD 单元的每个通道采用 same 向量的不同分量:
for(idx = 0; idx<size; idx+=1)
... = X[idx].coord(laneid) + Y[idx].coord(laneid);
垂直方法更容易实现。事实上,编译器已经在尝试自动矢量化。问题是随着 SIMD 单元宽度的增加,实现无法从中受益。如果您从 4-wide 切换到 16-wide SIMD,您仍然只会将 3 个数字与您的 3D 矢量并行相加。
水平方法更难。您通常必须处理不同的分支、函数调用等......并且 - 您希望将数据重新组织成数组结构 - 以便不同数据对象的相应字段在内存中彼此相邻。
现在,回到您的问题:SoA只有在您进行水平并行化时才有意义。当每个通道访问不同对象的相同字段时,SoA 允许用更好对齐的单个内存获取替换昂贵的收集指令。 如果您尝试进行垂直操作,如问题中的示例 - 甚至没有人会首先考虑进行 SoA - 访问同一对象的多个字段会导致“聚集”。
但是,对于随机访问,即使您进行水平并行化,SoA 也可能不是最佳选择。首先,拥有 SoA 没有任何好处,因为您仍然需要进行昂贵的收集。但是,由于同一对象的字段分布在内存中,因此每次加载都会到达不同的缓存通道。它不仅增加了内存带宽的使用,还可能导致缓存抖动。 这就是为什么 SoA 在随机访问方面效率不高的原因。
更好的解决方案是采用混合方法:您将数据打包在 Array-of-Structures-of-Arrays-of-SIMD-with-size 中。但那是另一回事了……
【讨论】:
可能想提一下 SoA 的病态案例——相同逻辑结构的两个组件存在缓存行争用(从一个组件读取会导致另一个组件卸载)【参考方案2】:是的,您似乎了解情况。
如果您从同一个结构中读取多个值,那么 CPU 将只需要为这些结构成员获取尽可能多的缓存行 - 如果结构成员布局合理,可能只需要一个。所以缓存可能看起来像这样(v
是你想要的值,空槽是其他值)
line 1: | v | | v | v | | | v | |
如果这些值都必须从单独的数组中读取,那么它必须为每个值获取整个缓存行。所以缓存可能看起来像
line 1: | | | v | | | | | |
line 2: | | | | | v | | | |
line 3: | | v | | | | | | |
line 4: | | | v | | | | | |
如果您按顺序处理数组,那很好 - 您很快就会需要获取的额外值。
但是,如果您没有按顺序工作(用书中的话说,您处于“随机或不连贯的情况”),那么每次获取超过您需要的内容将浪费缓存中的空间,并且您将最终会使用更多的内存带宽,而不是将所需的值放在一个结构中。
【讨论】:
以上是关于用于矢量化的随机读取上的结构阵列 (AoS) 与阵列结构 (SoA)的主要内容,如果未能解决你的问题,请参考以下文章